mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-04 14:28:56 +01:00
Migrate to null-safety
This commit is contained in:
parent
1a4779f465
commit
df26b7a8c8
97 changed files with 1380 additions and 1315 deletions
|
@ -18,11 +18,11 @@ class Account with EquatableMixin {
|
|||
}
|
||||
|
||||
Account copyWith({
|
||||
String scheme,
|
||||
String address,
|
||||
String username,
|
||||
String password,
|
||||
List<String> roots,
|
||||
String? scheme,
|
||||
String? address,
|
||||
String? username,
|
||||
String? password,
|
||||
List<String>? roots,
|
||||
}) {
|
||||
return Account(
|
||||
scheme ?? this.scheme,
|
||||
|
@ -39,7 +39,7 @@ class Account with EquatableMixin {
|
|||
"scheme: '$scheme', "
|
||||
"address: '$address', "
|
||||
"username: '$username', "
|
||||
"password: '${password?.isNotEmpty == true ? (kDebugMode ? password : '***') : null}', "
|
||||
"password: '${password.isNotEmpty == true ? (kDebugMode ? password : '***') : null}', "
|
||||
"roots: List {'${roots.join('\', \'')}'}, "
|
||||
"}";
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
|
@ -43,9 +42,9 @@ class Api {
|
|||
Future<Response> request(
|
||||
String method,
|
||||
String endpoint, {
|
||||
Map<String, String> header,
|
||||
String body,
|
||||
Uint8List bodyBytes,
|
||||
Map<String, String>? header,
|
||||
String? body,
|
||||
Uint8List? bodyBytes,
|
||||
bool isResponseString = true,
|
||||
}) async {
|
||||
final url = _makeUri(endpoint);
|
||||
|
@ -114,7 +113,7 @@ class _Files {
|
|||
Api _api;
|
||||
|
||||
Future<Response> delete({
|
||||
@required String path,
|
||||
required String path,
|
||||
}) async {
|
||||
try {
|
||||
return await _api.request("DELETE", path);
|
||||
|
@ -125,7 +124,7 @@ class _Files {
|
|||
}
|
||||
|
||||
Future<Response> get({
|
||||
@required String path,
|
||||
required String path,
|
||||
}) async {
|
||||
try {
|
||||
return await _api.request("GET", path, isResponseString: false);
|
||||
|
@ -136,9 +135,9 @@ class _Files {
|
|||
}
|
||||
|
||||
Future<Response> put({
|
||||
@required String path,
|
||||
required String path,
|
||||
String mime = "application/octet-stream",
|
||||
Uint8List content,
|
||||
required Uint8List content,
|
||||
}) async {
|
||||
try {
|
||||
return await _api.request(
|
||||
|
@ -156,8 +155,8 @@ class _Files {
|
|||
}
|
||||
|
||||
Future<Response> propfind({
|
||||
@required String path,
|
||||
int depth,
|
||||
required String path,
|
||||
int? depth,
|
||||
getlastmodified,
|
||||
getetag,
|
||||
getcontenttype,
|
||||
|
@ -176,8 +175,8 @@ class _Files {
|
|||
hasPreview,
|
||||
size,
|
||||
richWorkspace,
|
||||
Map<String, String> customNamespaces,
|
||||
List<String> customProperties,
|
||||
Map<String, String>? customNamespaces,
|
||||
List<String>? customProperties,
|
||||
}) async {
|
||||
try {
|
||||
final bool hasDavNs = (getlastmodified != null ||
|
||||
|
@ -287,10 +286,10 @@ class _Files {
|
|||
/// [namespaces] should be specified in the format {"URI": "prefix"}, eg,
|
||||
/// {"DAV:": "d"}
|
||||
Future<Response> proppatch({
|
||||
@required String path,
|
||||
Map<String, String> namespaces,
|
||||
Map<String, dynamic> set,
|
||||
List<String> remove,
|
||||
required String path,
|
||||
Map<String, String>? namespaces,
|
||||
Map<String, dynamic>? set,
|
||||
List<String>? remove,
|
||||
}) async {
|
||||
try {
|
||||
final ns = <String, String>{
|
||||
|
@ -300,7 +299,7 @@ class _Files {
|
|||
builder
|
||||
..processing("xml", "version=\"1.0\"")
|
||||
..element("d:propertyupdate", namespaces: ns, nest: () {
|
||||
if (set?.isNotEmpty == true) {
|
||||
if (set != null && set.isNotEmpty) {
|
||||
builder.element("d:set", nest: () {
|
||||
builder.element("d:prop", nest: () {
|
||||
for (final e in set.entries) {
|
||||
|
@ -311,7 +310,7 @@ class _Files {
|
|||
});
|
||||
});
|
||||
}
|
||||
if (remove?.isNotEmpty == true) {
|
||||
if (remove != null && remove.isNotEmpty) {
|
||||
builder.element("d:remove", nest: () {
|
||||
builder.element("d:prop", nest: () {
|
||||
for (final e in remove) {
|
||||
|
@ -331,7 +330,7 @@ class _Files {
|
|||
|
||||
/// A folder can be created by sending a MKCOL request to the folder
|
||||
Future<Response> mkcol({
|
||||
@required String path,
|
||||
required String path,
|
||||
}) async {
|
||||
try {
|
||||
return await _api.request("MKCOL", path);
|
||||
|
@ -344,9 +343,9 @@ class _Files {
|
|||
/// A file or folder can be copied by sending a COPY request to the file or
|
||||
/// folder and specifying the [destinationUrl] as full url
|
||||
Future<Response> copy({
|
||||
@required String path,
|
||||
@required String destinationUrl,
|
||||
bool overwrite,
|
||||
required String path,
|
||||
required String destinationUrl,
|
||||
bool? overwrite,
|
||||
}) async {
|
||||
try {
|
||||
return await _api.request("COPY", path, header: {
|
||||
|
@ -362,9 +361,9 @@ class _Files {
|
|||
/// A file or folder can be moved by sending a MOVE request to the file or
|
||||
/// folder and specifying the [destinationUrl] as full url
|
||||
Future<Response> move({
|
||||
@required String path,
|
||||
@required String destinationUrl,
|
||||
bool overwrite,
|
||||
required String path,
|
||||
required String destinationUrl,
|
||||
bool? overwrite,
|
||||
}) async {
|
||||
try {
|
||||
return await _api.request("MOVE", path, header: {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/// Helper functions working with remote Nextcloud server
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
|
@ -10,10 +9,10 @@ import 'package:nc_photos/exception.dart';
|
|||
String getFilePreviewUrl(
|
||||
Account account,
|
||||
File file, {
|
||||
@required int width,
|
||||
@required int height,
|
||||
String mode,
|
||||
bool a,
|
||||
required int width,
|
||||
required int height,
|
||||
String? mode,
|
||||
bool? a,
|
||||
}) {
|
||||
return "${account.url}/"
|
||||
"${getFilePreviewUrlRelative(file, width: width, height: height, mode: mode, a: a)}";
|
||||
|
@ -24,10 +23,10 @@ String getFilePreviewUrl(
|
|||
/// cropped
|
||||
String getFilePreviewUrlRelative(
|
||||
File file, {
|
||||
@required int width,
|
||||
@required int height,
|
||||
String mode,
|
||||
bool a,
|
||||
required int width,
|
||||
required int height,
|
||||
String? mode,
|
||||
bool? a,
|
||||
}) {
|
||||
String url;
|
||||
if (file.fileId != null) {
|
||||
|
@ -70,7 +69,7 @@ Future<String> exchangePassword(Account account) async {
|
|||
try {
|
||||
final appPwdRegex = RegExp(r"<apppassword>(.*)</apppassword>");
|
||||
final appPwdMatch = appPwdRegex.firstMatch(response.body);
|
||||
return appPwdMatch.group(1);
|
||||
return appPwdMatch!.group(1)!;
|
||||
} catch (_) {
|
||||
// this happens when the address is not the base URL and so Nextcloud
|
||||
// returned the login page
|
||||
|
|
|
@ -8,7 +8,6 @@ import 'package:nc_photos/entity/album/upgrader.dart';
|
|||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
class AppDb {
|
||||
|
@ -140,7 +139,7 @@ class AppDbAlbumEntry {
|
|||
upgraderV1: AlbumUpgraderV1(),
|
||||
upgraderV2: AlbumUpgraderV2(),
|
||||
upgraderV3: AlbumUpgraderV3(),
|
||||
),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -91,12 +91,12 @@ class AlbumSearchBloc extends Bloc<AlbumSearchBlocEvent, AlbumSearchBlocState> {
|
|||
_albums = ev.albums;
|
||||
if (_lastSearch != null) {
|
||||
// search again
|
||||
yield* _onEventSearch(_lastSearch);
|
||||
yield* _onEventSearch(_lastSearch!);
|
||||
}
|
||||
}
|
||||
|
||||
var _albums = <Album>[];
|
||||
AlbumSearchBlocSearchEvent _lastSearch;
|
||||
AlbumSearchBlocSearchEvent? _lastSearch;
|
||||
|
||||
static final _log = Logger("bloc.album_search.AlbumSearchBloc");
|
||||
}
|
||||
|
|
|
@ -112,12 +112,12 @@ class AlbumSearchSuggestionBloc extends Bloc<AlbumSearchSuggestionBlocEvent,
|
|||
}
|
||||
if (_lastSearch != null) {
|
||||
// search again
|
||||
yield* _onEventSearch(_lastSearch);
|
||||
yield* _onEventSearch(_lastSearch!);
|
||||
}
|
||||
}
|
||||
|
||||
final _search = Woozy(limit: 5);
|
||||
AlbumSearchSuggestionBlocSearchEvent _lastSearch;
|
||||
AlbumSearchSuggestionBlocSearchEvent? _lastSearch;
|
||||
|
||||
static final _log =
|
||||
Logger("bloc.album_search_suggestion.AlbumSearchSuggestionBloc");
|
||||
|
|
|
@ -47,7 +47,7 @@ abstract class ListAlbumBlocState {
|
|||
"}";
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final Account? account;
|
||||
final List<Album> albums;
|
||||
}
|
||||
|
||||
|
@ -56,18 +56,18 @@ class ListAlbumBlocInit extends ListAlbumBlocState {
|
|||
}
|
||||
|
||||
class ListAlbumBlocLoading extends ListAlbumBlocState {
|
||||
const ListAlbumBlocLoading(Account account, List<Album> albums)
|
||||
const ListAlbumBlocLoading(Account? account, List<Album> albums)
|
||||
: super(account, albums);
|
||||
}
|
||||
|
||||
class ListAlbumBlocSuccess extends ListAlbumBlocState {
|
||||
const ListAlbumBlocSuccess(Account account, List<Album> albums)
|
||||
const ListAlbumBlocSuccess(Account? account, List<Album> albums)
|
||||
: super(account, albums);
|
||||
}
|
||||
|
||||
class ListAlbumBlocFailure extends ListAlbumBlocState {
|
||||
const ListAlbumBlocFailure(
|
||||
Account account, List<Album> albums, this.exception)
|
||||
Account? account, List<Album> albums, this.exception)
|
||||
: super(account, albums);
|
||||
|
||||
@override
|
||||
|
@ -84,7 +84,7 @@ class ListAlbumBlocFailure extends ListAlbumBlocState {
|
|||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ListAlbumBlocInconsistent extends ListAlbumBlocState {
|
||||
const ListAlbumBlocInconsistent(Account account, List<Album> albums)
|
||||
const ListAlbumBlocInconsistent(Account? account, List<Album> albums)
|
||||
: super(account, albums);
|
||||
}
|
||||
|
||||
|
@ -134,7 +134,7 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
|
|||
if (newState is ListAlbumBlocFailure) {
|
||||
yield ListAlbumBlocFailure(
|
||||
ev.account,
|
||||
newState.albums?.isNotEmpty == true ? newState.albums : state.albums,
|
||||
newState.albums.isNotEmpty ? newState.albums : state.albums,
|
||||
newState.exception);
|
||||
} else {
|
||||
yield newState;
|
||||
|
@ -206,9 +206,9 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
|
|||
}
|
||||
}
|
||||
|
||||
AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
|
||||
AppEventListener<FileRemovedEvent> _fileRemovedListener;
|
||||
AppEventListener<AlbumCreatedEvent> _albumCreatedListener;
|
||||
late AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
|
||||
late AppEventListener<FileRemovedEvent> _fileRemovedListener;
|
||||
late AppEventListener<AlbumCreatedEvent> _albumCreatedListener;
|
||||
|
||||
static final _log = Logger("bloc.list_album.ListAlbumBloc");
|
||||
}
|
||||
|
|
|
@ -13,9 +13,11 @@ class LsDirBlocItem {
|
|||
if (isDeep) {
|
||||
return "$runtimeType:${_toDeepString(0)}";
|
||||
} else {
|
||||
final childrenStr =
|
||||
children == null ? "null" : "List {length: ${children!.length}}";
|
||||
return "$runtimeType {"
|
||||
"file: '${file.path}', "
|
||||
"children: List {length: ${children.length}}, "
|
||||
"children: $childrenStr, "
|
||||
"}";
|
||||
}
|
||||
}
|
||||
|
@ -23,19 +25,19 @@ class LsDirBlocItem {
|
|||
String _toDeepString(int level) {
|
||||
String product = "\n" + " " * (level * 2) + "-${file.path}";
|
||||
if (children != null) {
|
||||
for (final c in children) {
|
||||
for (final c in children!) {
|
||||
product += c._toDeepString(level + 1);
|
||||
}
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
File file;
|
||||
final File file;
|
||||
|
||||
/// Child directories under this directory
|
||||
///
|
||||
/// Null if this dir is not listed, due to things like depth limitation
|
||||
List<LsDirBlocItem> children;
|
||||
List<LsDirBlocItem>? children;
|
||||
}
|
||||
|
||||
abstract class LsDirBlocEvent {
|
||||
|
@ -59,9 +61,9 @@ class LsDirBlocQuery extends LsDirBlocEvent {
|
|||
}
|
||||
|
||||
LsDirBlocQuery copyWith({
|
||||
Account account,
|
||||
File root,
|
||||
int depth,
|
||||
Account? account,
|
||||
File? root,
|
||||
int? depth,
|
||||
}) {
|
||||
return LsDirBlocQuery(
|
||||
account ?? this.account,
|
||||
|
@ -76,11 +78,7 @@ class LsDirBlocQuery extends LsDirBlocEvent {
|
|||
}
|
||||
|
||||
abstract class LsDirBlocState {
|
||||
const LsDirBlocState(this._account, this._root, this._items);
|
||||
|
||||
Account get account => _account;
|
||||
File get root => _root;
|
||||
List<LsDirBlocItem> get items => _items;
|
||||
const LsDirBlocState(this.account, this.root, this.items);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
|
@ -91,9 +89,9 @@ abstract class LsDirBlocState {
|
|||
"}";
|
||||
}
|
||||
|
||||
final Account _account;
|
||||
final File _root;
|
||||
final List<LsDirBlocItem> _items;
|
||||
final Account? account;
|
||||
final File root;
|
||||
final List<LsDirBlocItem> items;
|
||||
}
|
||||
|
||||
class LsDirBlocInit extends LsDirBlocState {
|
||||
|
@ -101,18 +99,18 @@ class LsDirBlocInit extends LsDirBlocState {
|
|||
}
|
||||
|
||||
class LsDirBlocLoading extends LsDirBlocState {
|
||||
const LsDirBlocLoading(Account account, File root, List<LsDirBlocItem> items)
|
||||
const LsDirBlocLoading(Account? account, File root, List<LsDirBlocItem> items)
|
||||
: super(account, root, items);
|
||||
}
|
||||
|
||||
class LsDirBlocSuccess extends LsDirBlocState {
|
||||
const LsDirBlocSuccess(Account account, File root, List<LsDirBlocItem> items)
|
||||
const LsDirBlocSuccess(Account? account, File root, List<LsDirBlocItem> items)
|
||||
: super(account, root, items);
|
||||
}
|
||||
|
||||
class LsDirBlocFailure extends LsDirBlocState {
|
||||
const LsDirBlocFailure(
|
||||
Account account, File root, List<LsDirBlocItem> items, this.exception)
|
||||
Account? account, File root, List<LsDirBlocItem> items, this.exception)
|
||||
: super(account, root, items);
|
||||
|
||||
@override
|
||||
|
@ -153,12 +151,12 @@ class LsDirBloc extends Bloc<LsDirBlocEvent, LsDirBlocState> {
|
|||
var files = _cache[ev.root.path];
|
||||
if (files == null) {
|
||||
files = (await Ls(FileRepo(FileWebdavDataSource()))(ev.account, ev.root))
|
||||
.where((f) => f.isCollection)
|
||||
.where((f) => f.isCollection ?? false)
|
||||
.toList();
|
||||
_cache[ev.root.path] = files;
|
||||
}
|
||||
for (final f in files) {
|
||||
List<LsDirBlocItem> children;
|
||||
List<LsDirBlocItem>? children;
|
||||
if (ev.depth > 1) {
|
||||
children = await _query(ev.copyWith(root: f, depth: ev.depth - 1));
|
||||
}
|
||||
|
|
|
@ -58,10 +58,7 @@ class _ScanDirBlocExternalEvent extends ScanDirBlocEvent {
|
|||
}
|
||||
|
||||
abstract class ScanDirBlocState {
|
||||
const ScanDirBlocState(this._account, this._files);
|
||||
|
||||
Account get account => _account;
|
||||
List<File> get files => _files;
|
||||
const ScanDirBlocState(this.account, this.files);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
|
@ -71,8 +68,8 @@ abstract class ScanDirBlocState {
|
|||
"}";
|
||||
}
|
||||
|
||||
final Account _account;
|
||||
final List<File> _files;
|
||||
final Account? account;
|
||||
final List<File> files;
|
||||
}
|
||||
|
||||
class ScanDirBlocInit extends ScanDirBlocState {
|
||||
|
@ -80,17 +77,17 @@ class ScanDirBlocInit extends ScanDirBlocState {
|
|||
}
|
||||
|
||||
class ScanDirBlocLoading extends ScanDirBlocState {
|
||||
const ScanDirBlocLoading(Account account, List<File> files)
|
||||
const ScanDirBlocLoading(Account? account, List<File> files)
|
||||
: super(account, files);
|
||||
}
|
||||
|
||||
class ScanDirBlocSuccess extends ScanDirBlocState {
|
||||
const ScanDirBlocSuccess(Account account, List<File> files)
|
||||
const ScanDirBlocSuccess(Account? account, List<File> files)
|
||||
: super(account, files);
|
||||
}
|
||||
|
||||
class ScanDirBlocFailure extends ScanDirBlocState {
|
||||
const ScanDirBlocFailure(Account account, List<File> files, this.exception)
|
||||
const ScanDirBlocFailure(Account? account, List<File> files, this.exception)
|
||||
: super(account, files);
|
||||
|
||||
@override
|
||||
|
@ -107,7 +104,7 @@ class ScanDirBlocFailure extends ScanDirBlocState {
|
|||
/// The state of this bloc is inconsistent. This typically means that the data
|
||||
/// may have been changed externally
|
||||
class ScanDirBlocInconsistent extends ScanDirBlocState {
|
||||
const ScanDirBlocInconsistent(Account account, List<File> files)
|
||||
const ScanDirBlocInconsistent(Account? account, List<File> files)
|
||||
: super(account, files);
|
||||
}
|
||||
|
||||
|
@ -285,11 +282,12 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
|
|||
}
|
||||
}
|
||||
|
||||
AppEventListener<FileRemovedEvent> _fileRemovedEventListener;
|
||||
AppEventListener<FilePropertyUpdatedEvent> _filePropertyUpdatedEventListener;
|
||||
late AppEventListener<FileRemovedEvent> _fileRemovedEventListener;
|
||||
late AppEventListener<FilePropertyUpdatedEvent>
|
||||
_filePropertyUpdatedEventListener;
|
||||
|
||||
int _successivePropertyUpdatedCount = 0;
|
||||
StreamSubscription<void> _propertyUpdatedSubscription;
|
||||
StreamSubscription<void>? _propertyUpdatedSubscription;
|
||||
|
||||
bool _shouldCheckCache = true;
|
||||
|
||||
|
|
|
@ -7,12 +7,16 @@ class CancelableGetFile {
|
|||
|
||||
Future<FileInfo> getFileUntil(String key,
|
||||
{bool ignoreMemCache = false}) async {
|
||||
FileInfo product;
|
||||
FileInfo? product;
|
||||
while (product == null && _shouldRun) {
|
||||
product = await store.getFile(key, ignoreMemCache: ignoreMemCache);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
}
|
||||
return product ?? Future.error("Interrupted");
|
||||
if (product == null) {
|
||||
return Future.error("Interrupted");
|
||||
} else {
|
||||
return product;
|
||||
}
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:convert';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:idb_sqflite/idb_sqflite.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
|
@ -31,57 +30,57 @@ bool isAlbumFile(Account account, File file) =>
|
|||
/// Immutable object that represents an album
|
||||
class Album with EquatableMixin {
|
||||
Album({
|
||||
DateTime lastUpdated,
|
||||
@required String name,
|
||||
@required this.provider,
|
||||
@required this.coverProvider,
|
||||
@required this.sortProvider,
|
||||
DateTime? lastUpdated,
|
||||
required this.name,
|
||||
required this.provider,
|
||||
required this.coverProvider,
|
||||
required this.sortProvider,
|
||||
this.albumFile,
|
||||
}) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc(),
|
||||
this.name = name ?? "";
|
||||
}) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc();
|
||||
|
||||
factory Album.fromJson(
|
||||
static Album? fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
AlbumUpgraderV1 upgraderV1,
|
||||
AlbumUpgraderV2 upgraderV2,
|
||||
AlbumUpgraderV3 upgraderV3,
|
||||
required AlbumUpgraderV1? upgraderV1,
|
||||
required AlbumUpgraderV2? upgraderV2,
|
||||
required AlbumUpgraderV3? upgraderV3,
|
||||
}) {
|
||||
final jsonVersion = json["version"];
|
||||
Map<String, dynamic>? result = json;
|
||||
if (jsonVersion < 2) {
|
||||
json = upgraderV1?.call(json);
|
||||
if (json == null) {
|
||||
result = upgraderV1?.call(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 3) {
|
||||
json = upgraderV2?.call(json);
|
||||
if (json == null) {
|
||||
result = upgraderV2?.call(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 4) {
|
||||
json = upgraderV3?.call(json);
|
||||
if (json == null) {
|
||||
result = upgraderV3?.call(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return Album(
|
||||
lastUpdated: json["lastUpdated"] == null
|
||||
lastUpdated: result["lastUpdated"] == null
|
||||
? null
|
||||
: DateTime.parse(json["lastUpdated"]),
|
||||
name: json["name"],
|
||||
: DateTime.parse(result["lastUpdated"]),
|
||||
name: result["name"],
|
||||
provider:
|
||||
AlbumProvider.fromJson(json["provider"].cast<String, dynamic>()),
|
||||
AlbumProvider.fromJson(result["provider"].cast<String, dynamic>()),
|
||||
coverProvider: AlbumCoverProvider.fromJson(
|
||||
json["coverProvider"].cast<String, dynamic>()),
|
||||
result["coverProvider"].cast<String, dynamic>()),
|
||||
sortProvider: AlbumSortProvider.fromJson(
|
||||
json["sortProvider"].cast<String, dynamic>()),
|
||||
albumFile: json["albumFile"] == null
|
||||
result["sortProvider"].cast<String, dynamic>()),
|
||||
albumFile: result["albumFile"] == null
|
||||
? null
|
||||
: File.fromJson(json["albumFile"].cast<String, dynamic>()),
|
||||
: File.fromJson(result["albumFile"].cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -103,12 +102,12 @@ class Album with EquatableMixin {
|
|||
/// will be used. In order to keep [lastUpdated], you must explicitly assign
|
||||
/// it with value from this or a null value
|
||||
Album copyWith({
|
||||
OrNull<DateTime> lastUpdated,
|
||||
String name,
|
||||
AlbumProvider provider,
|
||||
AlbumCoverProvider coverProvider,
|
||||
AlbumSortProvider sortProvider,
|
||||
File albumFile,
|
||||
OrNull<DateTime>? lastUpdated,
|
||||
String? name,
|
||||
AlbumProvider? provider,
|
||||
AlbumCoverProvider? coverProvider,
|
||||
AlbumSortProvider? sortProvider,
|
||||
File? albumFile,
|
||||
}) {
|
||||
return Album(
|
||||
lastUpdated:
|
||||
|
@ -141,7 +140,7 @@ class Album with EquatableMixin {
|
|||
"provider": provider.toJson(),
|
||||
"coverProvider": coverProvider.toJson(),
|
||||
"sortProvider": sortProvider.toJson(),
|
||||
if (albumFile != null) "albumFile": albumFile.toJson(),
|
||||
if (albumFile != null) "albumFile": albumFile!.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -165,7 +164,7 @@ class Album with EquatableMixin {
|
|||
/// How is this album stored on server
|
||||
///
|
||||
/// This field is typically only meaningful when returned by [AlbumRepo.get]
|
||||
final File albumFile;
|
||||
final File? albumFile;
|
||||
|
||||
/// versioning of this class, use to upgrade old persisted album
|
||||
static const version = 4;
|
||||
|
@ -224,7 +223,8 @@ class AlbumRemoteDataSource implements AlbumDataSource {
|
|||
upgraderV1: AlbumUpgraderV1(),
|
||||
upgraderV2: AlbumUpgraderV2(),
|
||||
upgraderV3: AlbumUpgraderV3(),
|
||||
).copyWith(
|
||||
)!
|
||||
.copyWith(
|
||||
lastUpdated: OrNull(null),
|
||||
albumFile: albumFile,
|
||||
);
|
||||
|
@ -245,8 +245,8 @@ class AlbumRemoteDataSource implements AlbumDataSource {
|
|||
final filePath =
|
||||
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
||||
final fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(
|
||||
account, filePath, utf8.encode(jsonEncode(album.toRemoteJson())),
|
||||
await PutFileBinary(fileRepo)(account, filePath,
|
||||
Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||
shouldCreateMissingDir: true);
|
||||
// query album file
|
||||
final list = await Ls(fileRepo)(account, File(path: filePath),
|
||||
|
@ -256,10 +256,10 @@ class AlbumRemoteDataSource implements AlbumDataSource {
|
|||
|
||||
@override
|
||||
update(Account account, Album album) async {
|
||||
_log.info("[update] ${album.albumFile.path}");
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
final fileRepo = FileRepo(FileWebdavDataSource());
|
||||
await PutFileBinary(fileRepo)(account, album.albumFile.path,
|
||||
utf8.encode(jsonEncode(album.toRemoteJson())));
|
||||
await PutFileBinary(fileRepo)(account, album.albumFile!.path,
|
||||
Utf8Encoder().convert(jsonEncode(album.toRemoteJson())));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -286,7 +286,7 @@ class AlbumAppDbDataSource implements AlbumDataSource {
|
|||
final path = AppDbAlbumEntry.toPathFromFile(account, albumFile);
|
||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||
final List results = await index.getAll(range);
|
||||
if (results?.isNotEmpty == true) {
|
||||
if (results.isNotEmpty == true) {
|
||||
final entries = results
|
||||
.map((e) => AppDbAlbumEntry.fromJson(e.cast<String, dynamic>()));
|
||||
if (entries.length > 1) {
|
||||
|
@ -317,7 +317,7 @@ class AlbumAppDbDataSource implements AlbumDataSource {
|
|||
|
||||
@override
|
||||
update(Account account, Album album) {
|
||||
_log.info("[update] ${album.albumFile.path}");
|
||||
_log.info("[update] ${album.albumFile!.path}");
|
||||
return AppDb.use((db) async {
|
||||
final transaction =
|
||||
db.transaction(AppDb.albumStoreName, idbModeReadWrite);
|
||||
|
@ -337,8 +337,8 @@ class AlbumCachedDataSource implements AlbumDataSource {
|
|||
get(Account account, File albumFile) async {
|
||||
try {
|
||||
final cache = await _appDbSrc.get(account, albumFile);
|
||||
if (cache.albumFile.etag?.isNotEmpty == true &&
|
||||
cache.albumFile.etag == albumFile.etag) {
|
||||
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||
cache.albumFile!.etag == albumFile.etag) {
|
||||
// cache is good
|
||||
_log.fine(
|
||||
"[get] etag matched for ${AppDbAlbumEntry.toPathFromFile(account, albumFile)}");
|
||||
|
@ -412,7 +412,7 @@ class AlbumCachedDataSource implements AlbumDataSource {
|
|||
Future<void> _cacheAlbum(
|
||||
ObjectStore store, Account account, Album album) async {
|
||||
final index = store.index(AppDbAlbumEntry.indexName);
|
||||
final path = AppDbAlbumEntry.toPathFromFile(account, album.albumFile);
|
||||
final path = AppDbAlbumEntry.toPathFromFile(account, album.albumFile!);
|
||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||
// count number of entries for this album
|
||||
final count = await index.count(range);
|
||||
|
@ -441,7 +441,7 @@ Future<void> _cacheAlbum(
|
|||
for (final e in entries) {
|
||||
_log.info("[_cacheAlbum] Caching ${e.path}[${e.index}]");
|
||||
await store.put(e.toJson(),
|
||||
AppDbAlbumEntry.toPrimaryKey(account, e.album.albumFile, e.index));
|
||||
AppDbAlbumEntry.toPrimaryKey(account, e.album.albumFile!, e.index));
|
||||
}
|
||||
|
||||
if (count > entries.length) {
|
||||
|
|
|
@ -40,7 +40,7 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
|||
@override
|
||||
toString();
|
||||
|
||||
File getCover(Album album);
|
||||
File? getCover(Album album);
|
||||
|
||||
Map<String, dynamic> _toContentJson();
|
||||
|
||||
|
@ -68,7 +68,8 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
|||
"}";
|
||||
}
|
||||
|
||||
File getCover(Album album) {
|
||||
@override
|
||||
getCover(Album album) {
|
||||
if (coverFile == null) {
|
||||
try {
|
||||
// use the latest file as cover
|
||||
|
@ -77,7 +78,8 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
|||
.whereType<AlbumFileItem>()
|
||||
.map((e) => e.file)
|
||||
.where((element) =>
|
||||
file_util.isSupportedFormat(element) && element.hasPreview)
|
||||
file_util.isSupportedFormat(element) &&
|
||||
(element.hasPreview ?? false))
|
||||
.sorted(compareFileDateTimeDescending)
|
||||
.first;
|
||||
} catch (_) {
|
||||
|
@ -96,11 +98,11 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
|||
@override
|
||||
_toContentJson() {
|
||||
return {
|
||||
if (coverFile != null) "coverFile": coverFile.toJson(),
|
||||
if (coverFile != null) "coverFile": coverFile!.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
final File coverFile;
|
||||
final File? coverFile;
|
||||
|
||||
static const _type = "auto";
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/list_extension.dart';
|
||||
|
@ -60,18 +59,16 @@ abstract class AlbumItem {
|
|||
|
||||
class AlbumFileItem extends AlbumItem with EquatableMixin {
|
||||
AlbumFileItem({
|
||||
@required this.file,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
// ignore: hash_and_equals
|
||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
||||
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||
|
||||
bool equals(Object other, {bool isDeep = false}) {
|
||||
bool equals(Object? other, {bool isDeep = false}) {
|
||||
if (other is AlbumFileItem) {
|
||||
return super == other &&
|
||||
(file == null) == (other.file == null) &&
|
||||
(file?.equals(other.file, isDeep: isDeep) ?? true);
|
||||
return super == other && (file.equals(other.file, isDeep: isDeep));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -109,7 +106,7 @@ class AlbumFileItem extends AlbumItem with EquatableMixin {
|
|||
|
||||
class AlbumLabelItem extends AlbumItem with EquatableMixin {
|
||||
AlbumLabelItem({
|
||||
@required this.text,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
factory AlbumLabelItem.fromJson(Map<String, dynamic> json) {
|
||||
|
|
|
@ -50,7 +50,7 @@ abstract class AlbumProvider with EquatableMixin {
|
|||
toString({bool isDeep = false});
|
||||
|
||||
/// Return the date time associated with the latest item, or null
|
||||
DateTime get latestItemTime;
|
||||
DateTime? get latestItemTime;
|
||||
|
||||
AlbumProvider copyWith();
|
||||
|
||||
|
@ -59,7 +59,7 @@ abstract class AlbumProvider with EquatableMixin {
|
|||
|
||||
class AlbumStaticProvider extends AlbumProvider {
|
||||
AlbumStaticProvider({
|
||||
@required List<AlbumItem> items,
|
||||
required List<AlbumItem> items,
|
||||
}) : this.items = UnmodifiableListView(items);
|
||||
|
||||
factory AlbumStaticProvider.fromJson(Map<String, dynamic> json) {
|
||||
|
@ -90,7 +90,9 @@ class AlbumStaticProvider extends AlbumProvider {
|
|||
}
|
||||
|
||||
@override
|
||||
AlbumStaticProvider copyWith({List<AlbumItem> items}) {
|
||||
AlbumStaticProvider copyWith({
|
||||
List<AlbumItem>? items,
|
||||
}) {
|
||||
return AlbumStaticProvider(
|
||||
items: items ?? this.items,
|
||||
);
|
||||
|
@ -124,7 +126,7 @@ class AlbumStaticProvider extends AlbumProvider {
|
|||
|
||||
abstract class AlbumDynamicProvider extends AlbumProvider {
|
||||
AlbumDynamicProvider({
|
||||
DateTime latestItemTime,
|
||||
DateTime? latestItemTime,
|
||||
}) : _latestItemTime = latestItemTime;
|
||||
|
||||
@override
|
||||
|
@ -137,13 +139,13 @@ abstract class AlbumDynamicProvider extends AlbumProvider {
|
|||
@override
|
||||
toContentJson() {
|
||||
return {
|
||||
"latestItemTime": _latestItemTime?.toUtc()?.toIso8601String(),
|
||||
"latestItemTime": _latestItemTime?.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
AlbumDynamicProvider copyWith({
|
||||
DateTime latestItemTime,
|
||||
DateTime? latestItemTime,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -154,13 +156,13 @@ abstract class AlbumDynamicProvider extends AlbumProvider {
|
|||
_latestItemTime,
|
||||
];
|
||||
|
||||
final DateTime _latestItemTime;
|
||||
final DateTime? _latestItemTime;
|
||||
}
|
||||
|
||||
class AlbumDirProvider extends AlbumDynamicProvider {
|
||||
AlbumDirProvider({
|
||||
@required this.dirs,
|
||||
DateTime latestItemTime,
|
||||
required this.dirs,
|
||||
DateTime? latestItemTime,
|
||||
}) : super(latestItemTime: latestItemTime);
|
||||
|
||||
factory AlbumDirProvider.fromJson(Map<String, dynamic> json) {
|
||||
|
@ -192,8 +194,8 @@ class AlbumDirProvider extends AlbumDynamicProvider {
|
|||
|
||||
@override
|
||||
AlbumDirProvider copyWith({
|
||||
List<File> dirs,
|
||||
DateTime latestItemTime,
|
||||
List<File>? dirs,
|
||||
DateTime? latestItemTime,
|
||||
}) {
|
||||
return AlbumDirProvider(
|
||||
dirs: dirs ?? this.dirs,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/entity/album/item.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
|
@ -80,7 +79,7 @@ class AlbumNullSortProvider extends AlbumSortProvider {
|
|||
|
||||
abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
|
||||
const AlbumReversibleSortProvider({
|
||||
@required this.isAscending,
|
||||
required this.isAscending,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -108,7 +107,7 @@ abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
|
|||
/// Sort based on the time of the files
|
||||
class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
||||
const AlbumTimeSortProvider({
|
||||
bool isAscending,
|
||||
required bool isAscending,
|
||||
}) : super(isAscending: isAscending);
|
||||
|
||||
factory AlbumTimeSortProvider.fromJson(Map<String, dynamic> json) {
|
||||
|
@ -126,7 +125,7 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
|||
|
||||
@override
|
||||
sort(List<AlbumItem> items) {
|
||||
DateTime prevFileTime;
|
||||
DateTime? prevFileTime;
|
||||
return items
|
||||
.map((e) {
|
||||
if (e is AlbumFileItem) {
|
||||
|
@ -139,16 +138,16 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
|||
.stableSorted((x, y) {
|
||||
if (x.item1 == null && y.item1 == null) {
|
||||
return 0;
|
||||
}
|
||||
else if (x.item1 == null) {
|
||||
} else if (x.item1 == null) {
|
||||
return -1;
|
||||
} else if (y.item1 == null) {
|
||||
return 1;
|
||||
}
|
||||
if (isAscending) {
|
||||
return x.item1.compareTo(y.item1);
|
||||
} else {
|
||||
return y.item1.compareTo(x.item1);
|
||||
if (isAscending) {
|
||||
return x.item1!.compareTo(y.item1!);
|
||||
} else {
|
||||
return y.item1!.compareTo(x.item1!);
|
||||
}
|
||||
}
|
||||
})
|
||||
.map((e) => e.item2)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:logging/logging.dart';
|
||||
|
||||
abstract class AlbumUpgrader {
|
||||
Map<String, dynamic> call(Map<String, dynamic> json);
|
||||
Map<String, dynamic>? call(Map<String, dynamic> json);
|
||||
}
|
||||
|
||||
/// Upgrade v1 Album to v2
|
||||
|
@ -10,7 +10,8 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
|
|||
this.logFilePath,
|
||||
});
|
||||
|
||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
||||
@override
|
||||
call(Map<String, dynamic> json) {
|
||||
// v1 album items are corrupted in one of the updates, drop it
|
||||
_log.fine("[call] Upgrade v1 Album for file: $logFilePath");
|
||||
final result = Map<String, dynamic>.from(json);
|
||||
|
@ -19,7 +20,7 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
|
|||
}
|
||||
|
||||
/// File path for logging only
|
||||
final String logFilePath;
|
||||
final String? logFilePath;
|
||||
|
||||
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV1");
|
||||
}
|
||||
|
@ -30,7 +31,8 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
|||
this.logFilePath,
|
||||
});
|
||||
|
||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
||||
@override
|
||||
call(Map<String, dynamic> json) {
|
||||
// move v2 items to v3 provider
|
||||
_log.fine("[call] Upgrade v2 Album for file: $logFilePath");
|
||||
final result = Map<String, dynamic>.from(json);
|
||||
|
@ -51,7 +53,7 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
|||
}
|
||||
|
||||
/// File path for logging only
|
||||
final String logFilePath;
|
||||
final String? logFilePath;
|
||||
|
||||
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV2");
|
||||
}
|
||||
|
@ -62,7 +64,8 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
|||
this.logFilePath,
|
||||
});
|
||||
|
||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
||||
@override
|
||||
call(Map<String, dynamic> json) {
|
||||
// move v3 items to v4 provider
|
||||
_log.fine("[call] Upgrade v3 Album for file: $logFilePath");
|
||||
final result = Map<String, dynamic>.from(json);
|
||||
|
@ -77,7 +80,7 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
|||
}
|
||||
|
||||
/// File path for logging only
|
||||
final String logFilePath;
|
||||
final String? logFilePath;
|
||||
|
||||
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV3");
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@ class Exif with EquatableMixin {
|
|||
|
||||
@override
|
||||
// ignore: hash_and_equals
|
||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
||||
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||
|
||||
/// Compare two Exif objects
|
||||
///
|
||||
/// If [isDeep] is false, two Exif objects are considered identical if they
|
||||
/// contain the same number of fields. This hack is to save time comparing a
|
||||
/// large amount of data that are mostly immutable
|
||||
bool equals(Object other, {bool isDeep = false}) {
|
||||
bool equals(Object? other, {bool isDeep = false}) {
|
||||
if (isDeep) {
|
||||
return super == other;
|
||||
} else {
|
||||
|
@ -87,33 +87,33 @@ class Exif with EquatableMixin {
|
|||
}
|
||||
|
||||
/// 0x010f Make
|
||||
String get make => data["Make"];
|
||||
String? get make => data["Make"];
|
||||
|
||||
/// 0x0110 Model
|
||||
String get model => data["Model"];
|
||||
String? get model => data["Model"];
|
||||
|
||||
/// 0x9003 DateTimeOriginal
|
||||
DateTime get dateTimeOriginal => data.containsKey("DateTimeOriginal")
|
||||
DateTime? get dateTimeOriginal => data.containsKey("DateTimeOriginal")
|
||||
? dateTimeFormat.parse(data["DateTimeOriginal"]).toUtc()
|
||||
: null;
|
||||
|
||||
/// 0x829a ExposureTime
|
||||
Rational get exposureTime => data["ExposureTime"];
|
||||
Rational? get exposureTime => data["ExposureTime"];
|
||||
|
||||
/// 0x829d FNumber
|
||||
Rational get fNumber => data["FNumber"];
|
||||
Rational? get fNumber => data["FNumber"];
|
||||
|
||||
/// 0x8827 ISO/ISOSpeedRatings/PhotographicSensitivity
|
||||
int get isoSpeedRatings => data["ISOSpeedRatings"];
|
||||
int? get isoSpeedRatings => data["ISOSpeedRatings"];
|
||||
|
||||
/// 0x920a FocalLength
|
||||
Rational get focalLength => data["FocalLength"];
|
||||
Rational? get focalLength => data["FocalLength"];
|
||||
|
||||
/// 0x8825 GPS tags
|
||||
String get gpsLatitudeRef => data["GPSLatitudeRef"];
|
||||
List<Rational> get gpsLatitude => data["GPSLatitude"].cast<Rational>();
|
||||
String get gpsLongitudeRef => data["GPSLongitudeRef"];
|
||||
List<Rational> get gpsLongitude => data["GPSLongitude"].cast<Rational>();
|
||||
String? get gpsLatitudeRef => data["GPSLatitudeRef"];
|
||||
List<Rational>? get gpsLatitude => data["GPSLatitude"].cast<Rational>();
|
||||
String? get gpsLongitudeRef => data["GPSLongitudeRef"];
|
||||
List<Rational>? get gpsLongitude => data["GPSLongitude"].cast<Rational>();
|
||||
|
||||
@override
|
||||
get props => [
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/entity/exif.dart';
|
||||
|
@ -21,7 +20,7 @@ int compareFileDateTimeDescending(File x, File y) {
|
|||
/// Immutable object that hold metadata of a [File]
|
||||
class Metadata with EquatableMixin {
|
||||
Metadata({
|
||||
DateTime lastUpdated,
|
||||
DateTime? lastUpdated,
|
||||
this.fileEtag,
|
||||
this.imageWidth,
|
||||
this.imageHeight,
|
||||
|
@ -30,9 +29,9 @@ class Metadata with EquatableMixin {
|
|||
|
||||
@override
|
||||
// ignore: hash_and_equals
|
||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
||||
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||
|
||||
bool equals(Object other, {bool isDeep = false}) {
|
||||
bool equals(Object? other, {bool isDeep = false}) {
|
||||
if (other is Metadata) {
|
||||
return super == other &&
|
||||
(exif == null) == (other.exif == null) &&
|
||||
|
@ -48,36 +47,37 @@ class Metadata with EquatableMixin {
|
|||
/// corresponding upgrader will be called one by one to upgrade the json,
|
||||
/// version by version until it reached the active version. If any upgrader
|
||||
/// in the chain is null, the upgrade process will fail
|
||||
factory Metadata.fromJson(
|
||||
static Metadata? fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
MetadataUpgraderV1 upgraderV1,
|
||||
MetadataUpgraderV2 upgraderV2,
|
||||
required MetadataUpgraderV1? upgraderV1,
|
||||
required MetadataUpgraderV2? upgraderV2,
|
||||
}) {
|
||||
final jsonVersion = json["version"];
|
||||
Map<String, dynamic>? result = json;
|
||||
if (jsonVersion < 2) {
|
||||
json = upgraderV1?.call(json);
|
||||
if (json == null) {
|
||||
result = upgraderV1?.call(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (jsonVersion < 3) {
|
||||
json = upgraderV2?.call(json);
|
||||
if (json == null) {
|
||||
result = upgraderV2?.call(result);
|
||||
if (result == null) {
|
||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return Metadata(
|
||||
lastUpdated: json["lastUpdated"] == null
|
||||
lastUpdated: result["lastUpdated"] == null
|
||||
? null
|
||||
: DateTime.parse(json["lastUpdated"]),
|
||||
fileEtag: json["fileEtag"],
|
||||
imageWidth: json["imageWidth"],
|
||||
imageHeight: json["imageHeight"],
|
||||
exif: json["exif"] == null
|
||||
: DateTime.parse(result["lastUpdated"]),
|
||||
fileEtag: result["fileEtag"],
|
||||
imageWidth: result["imageWidth"],
|
||||
imageHeight: result["imageHeight"],
|
||||
exif: result["exif"] == null
|
||||
? null
|
||||
: Exif.fromJson(json["exif"].cast<String, dynamic>()),
|
||||
: Exif.fromJson(result["exif"].cast<String, dynamic>()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ class Metadata with EquatableMixin {
|
|||
if (fileEtag != null) "fileEtag": fileEtag,
|
||||
if (imageWidth != null) "imageWidth": imageWidth,
|
||||
if (imageHeight != null) "imageHeight": imageHeight,
|
||||
if (exif != null) "exif": exif.toJson(),
|
||||
if (exif != null) "exif": exif!.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -123,10 +123,10 @@ class Metadata with EquatableMixin {
|
|||
final DateTime lastUpdated;
|
||||
|
||||
/// Etag of the parent file when the metadata is saved
|
||||
final String fileEtag;
|
||||
final int imageWidth;
|
||||
final int imageHeight;
|
||||
final Exif exif;
|
||||
final String? fileEtag;
|
||||
final int? imageWidth;
|
||||
final int? imageHeight;
|
||||
final Exif? exif;
|
||||
|
||||
/// versioning of this class, use to upgrade old persisted metadata
|
||||
static const version = 3;
|
||||
|
@ -135,17 +135,17 @@ class Metadata with EquatableMixin {
|
|||
}
|
||||
|
||||
abstract class MetadataUpgrader {
|
||||
Map<String, dynamic> call(Map<String, dynamic> json);
|
||||
Map<String, dynamic>? call(Map<String, dynamic> json);
|
||||
}
|
||||
|
||||
/// Upgrade v1 Metadata to v2
|
||||
class MetadataUpgraderV1 implements MetadataUpgrader {
|
||||
MetadataUpgraderV1({
|
||||
@required this.fileContentType,
|
||||
required this.fileContentType,
|
||||
this.logFilePath,
|
||||
});
|
||||
|
||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
||||
Map<String, dynamic>? call(Map<String, dynamic> json) {
|
||||
if (fileContentType == "image/webp") {
|
||||
// Version 1 metadata for webp is bugged, drop it
|
||||
_log.fine("[call] Upgrade v1 metadata for file: $logFilePath");
|
||||
|
@ -155,10 +155,10 @@ class MetadataUpgraderV1 implements MetadataUpgrader {
|
|||
}
|
||||
}
|
||||
|
||||
final String fileContentType;
|
||||
final String? fileContentType;
|
||||
|
||||
/// File path for logging only
|
||||
final String logFilePath;
|
||||
final String? logFilePath;
|
||||
|
||||
static final _log = Logger("entity.file.MetadataUpgraderV1");
|
||||
}
|
||||
|
@ -166,11 +166,11 @@ class MetadataUpgraderV1 implements MetadataUpgrader {
|
|||
/// Upgrade v2 Metadata to v3
|
||||
class MetadataUpgraderV2 implements MetadataUpgrader {
|
||||
MetadataUpgraderV2({
|
||||
@required this.fileContentType,
|
||||
required this.fileContentType,
|
||||
this.logFilePath,
|
||||
});
|
||||
|
||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
||||
Map<String, dynamic>? call(Map<String, dynamic> json) {
|
||||
if (fileContentType == "image/jpeg") {
|
||||
// Version 2 metadata for jpeg doesn't consider orientation
|
||||
if (json["exif"] != null && json["exif"].containsKey("Orientation")) {
|
||||
|
@ -187,17 +187,17 @@ class MetadataUpgraderV2 implements MetadataUpgrader {
|
|||
return json;
|
||||
}
|
||||
|
||||
final String fileContentType;
|
||||
final String? fileContentType;
|
||||
|
||||
/// File path for logging only
|
||||
final String logFilePath;
|
||||
final String? logFilePath;
|
||||
|
||||
static final _log = Logger("entity.file.MetadataUpgraderV2");
|
||||
}
|
||||
|
||||
class File with EquatableMixin {
|
||||
File({
|
||||
@required String path,
|
||||
required String path,
|
||||
this.contentLength,
|
||||
this.contentType,
|
||||
this.etag,
|
||||
|
@ -214,9 +214,9 @@ class File with EquatableMixin {
|
|||
|
||||
@override
|
||||
// ignore: hash_and_equals
|
||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
||||
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||
|
||||
bool equals(Object other, {bool isDeep = false}) {
|
||||
bool equals(Object? other, {bool isDeep = false}) {
|
||||
if (other is File) {
|
||||
return super == other &&
|
||||
(metadata == null) == (other.metadata == null) &&
|
||||
|
@ -310,33 +310,33 @@ class File with EquatableMixin {
|
|||
if (contentType != null) "contentType": contentType,
|
||||
if (etag != null) "etag": etag,
|
||||
if (lastModified != null)
|
||||
"lastModified": lastModified.toUtc().toIso8601String(),
|
||||
"lastModified": lastModified!.toUtc().toIso8601String(),
|
||||
if (isCollection != null) "isCollection": isCollection,
|
||||
if (usedBytes != null) "usedBytes": usedBytes,
|
||||
if (hasPreview != null) "hasPreview": hasPreview,
|
||||
if (fileId != null) "fileId": fileId,
|
||||
if (ownerId != null) "ownerId": ownerId,
|
||||
if (metadata != null) "metadata": metadata.toJson(),
|
||||
if (metadata != null) "metadata": metadata!.toJson(),
|
||||
if (isArchived != null) "isArchived": isArchived,
|
||||
if (overrideDateTime != null)
|
||||
"overrideDateTime": overrideDateTime.toUtc().toIso8601String(),
|
||||
"overrideDateTime": overrideDateTime!.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
File copyWith({
|
||||
String path,
|
||||
int contentLength,
|
||||
String contentType,
|
||||
String etag,
|
||||
DateTime lastModified,
|
||||
bool isCollection,
|
||||
int usedBytes,
|
||||
bool hasPreview,
|
||||
int fileId,
|
||||
String ownerId,
|
||||
OrNull<Metadata> metadata,
|
||||
OrNull<bool> isArchived,
|
||||
OrNull<DateTime> overrideDateTime,
|
||||
String? path,
|
||||
int? contentLength,
|
||||
String? contentType,
|
||||
String? etag,
|
||||
DateTime? lastModified,
|
||||
bool? isCollection,
|
||||
int? usedBytes,
|
||||
bool? hasPreview,
|
||||
int? fileId,
|
||||
String? ownerId,
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
}) {
|
||||
return File(
|
||||
path: path ?? this.path,
|
||||
|
@ -392,20 +392,20 @@ class File with EquatableMixin {
|
|||
];
|
||||
|
||||
final String path;
|
||||
final int contentLength;
|
||||
final String contentType;
|
||||
final String etag;
|
||||
final DateTime lastModified;
|
||||
final bool isCollection;
|
||||
final int usedBytes;
|
||||
final bool hasPreview;
|
||||
final int? contentLength;
|
||||
final String? contentType;
|
||||
final String? etag;
|
||||
final DateTime? lastModified;
|
||||
final bool? isCollection;
|
||||
final int? usedBytes;
|
||||
final bool? hasPreview;
|
||||
// maybe null when loaded from old cache
|
||||
final int fileId;
|
||||
final String ownerId;
|
||||
final int? fileId;
|
||||
final String? ownerId;
|
||||
// metadata
|
||||
final Metadata metadata;
|
||||
final bool isArchived;
|
||||
final DateTime overrideDateTime;
|
||||
final Metadata? metadata;
|
||||
final bool? isArchived;
|
||||
final DateTime? overrideDateTime;
|
||||
}
|
||||
|
||||
extension FileExtension on File {
|
||||
|
@ -442,9 +442,9 @@ class FileRepo {
|
|||
Future<void> updateProperty(
|
||||
Account account,
|
||||
File file, {
|
||||
OrNull<Metadata> metadata,
|
||||
OrNull<bool> isArchived,
|
||||
OrNull<DateTime> overrideDateTime,
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
}) =>
|
||||
this.dataSrc.updateProperty(
|
||||
account,
|
||||
|
@ -459,7 +459,7 @@ class FileRepo {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) =>
|
||||
this.dataSrc.copy(
|
||||
account,
|
||||
|
@ -473,7 +473,7 @@ class FileRepo {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) =>
|
||||
this.dataSrc.move(
|
||||
account,
|
||||
|
@ -506,9 +506,9 @@ abstract class FileDataSource {
|
|||
Future<void> updateProperty(
|
||||
Account account,
|
||||
File f, {
|
||||
OrNull<Metadata> metadata,
|
||||
OrNull<bool> isArchived,
|
||||
OrNull<DateTime> overrideDateTime,
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
});
|
||||
|
||||
/// Copy [f] to [destination]
|
||||
|
@ -519,7 +519,7 @@ abstract class FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
});
|
||||
|
||||
/// Move [f] to [destination]
|
||||
|
@ -530,7 +530,7 @@ abstract class FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
});
|
||||
|
||||
/// Create a directory at [path]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:idb_shim/idb_client.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
|
@ -27,7 +26,7 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
list(
|
||||
Account account,
|
||||
File f, {
|
||||
int depth,
|
||||
int? depth,
|
||||
}) async {
|
||||
_log.fine("[list] ${f.path}");
|
||||
final response = await Api(account).files().propfind(
|
||||
|
@ -61,7 +60,7 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
final files = WebdavFileParser()(xml);
|
||||
// _log.fine("[list] Parsed files: [$files]");
|
||||
return files.where((element) => _validateFile(element)).map((e) {
|
||||
if (e.metadata == null || e.metadata.fileEtag == e.etag) {
|
||||
if (e.metadata == null || e.metadata!.fileEtag == e.etag) {
|
||||
return e;
|
||||
} else {
|
||||
_log.info("[list] Ignore outdated metadata for ${e.path}");
|
||||
|
@ -112,27 +111,27 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
updateProperty(
|
||||
Account account,
|
||||
File f, {
|
||||
OrNull<Metadata> metadata,
|
||||
OrNull<bool> isArchived,
|
||||
OrNull<DateTime> overrideDateTime,
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
}) async {
|
||||
_log.info("[updateProperty] ${f.path}");
|
||||
if (metadata?.obj != null && metadata.obj.fileEtag != f.etag) {
|
||||
if (metadata?.obj != null && metadata!.obj!.fileEtag != f.etag) {
|
||||
_log.warning(
|
||||
"[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj.fileEtag}, file: ${f.etag})");
|
||||
"[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj!.fileEtag}, file: ${f.etag})");
|
||||
}
|
||||
final setProps = {
|
||||
if (metadata?.obj != null)
|
||||
"app:metadata": jsonEncode(metadata.obj.toJson()),
|
||||
if (isArchived?.obj != null) "app:is-archived": isArchived.obj,
|
||||
"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(),
|
||||
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",
|
||||
if (OrNull.isSetNull(metadata)) "app:metadata",
|
||||
if (OrNull.isSetNull(isArchived)) "app:is-archived",
|
||||
if (OrNull.isSetNull(overrideDateTime)) "app:override-date-time",
|
||||
];
|
||||
final response = await Api(account).files().proppatch(
|
||||
path: f.path,
|
||||
|
@ -155,7 +154,7 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) async {
|
||||
_log.info("[copy] ${f.path} to $destination");
|
||||
final response = await Api(account).files().copy(
|
||||
|
@ -176,7 +175,7 @@ class FileWebdavDataSource implements FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) async {
|
||||
_log.info("[move] ${f.path} to $destination");
|
||||
final response = await Api(account).files().move(
|
||||
|
@ -256,9 +255,9 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
updateProperty(
|
||||
Account account,
|
||||
File f, {
|
||||
OrNull<Metadata> metadata,
|
||||
OrNull<bool> isArchived,
|
||||
OrNull<DateTime> overrideDateTime,
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
}) {
|
||||
_log.info("[updateProperty] ${f.path}");
|
||||
return AppDb.use((db) async {
|
||||
|
@ -300,7 +299,7 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) async {
|
||||
// do nothing
|
||||
}
|
||||
|
@ -310,7 +309,7 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) async {
|
||||
// do nothing
|
||||
}
|
||||
|
@ -325,7 +324,7 @@ class FileAppDbDataSource implements FileDataSource {
|
|||
final path = AppDbFileEntry.toPath(account, f);
|
||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||
final List results = await index.getAll(range);
|
||||
if (results?.isNotEmpty == true) {
|
||||
if (results.isNotEmpty == true) {
|
||||
final entries = results
|
||||
.map((e) => AppDbFileEntry.fromJson(e.cast<String, dynamic>()));
|
||||
return entries
|
||||
|
@ -358,7 +357,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
);
|
||||
final cache = await cacheManager.list(account, f);
|
||||
if (cacheManager.isGood) {
|
||||
return cache;
|
||||
return cache!;
|
||||
}
|
||||
|
||||
// no cache or outdated
|
||||
|
@ -426,9 +425,9 @@ class FileCachedDataSource implements FileDataSource {
|
|||
updateProperty(
|
||||
Account account,
|
||||
File f, {
|
||||
OrNull<Metadata> metadata,
|
||||
OrNull<bool> isArchived,
|
||||
OrNull<DateTime> overrideDateTime,
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
}) async {
|
||||
await _remoteSrc
|
||||
.updateProperty(
|
||||
|
@ -462,7 +461,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) async {
|
||||
await _remoteSrc.copy(account, f, destination,
|
||||
shouldOverwrite: shouldOverwrite);
|
||||
|
@ -473,7 +472,7 @@ class FileCachedDataSource implements FileDataSource {
|
|||
Account account,
|
||||
File f,
|
||||
String destination, {
|
||||
bool shouldOverwrite,
|
||||
bool? shouldOverwrite,
|
||||
}) async {
|
||||
await _remoteSrc.move(account, f, destination,
|
||||
shouldOverwrite: shouldOverwrite);
|
||||
|
@ -599,17 +598,17 @@ class FileCachedDataSource implements FileDataSource {
|
|||
|
||||
class _CacheManager {
|
||||
_CacheManager({
|
||||
@required this.appDbSrc,
|
||||
@required this.remoteSrc,
|
||||
required this.appDbSrc,
|
||||
required this.remoteSrc,
|
||||
this.shouldCheckCache = false,
|
||||
});
|
||||
|
||||
/// Return the cached results of listing a directory [f]
|
||||
///
|
||||
/// Should check [isGood] before using the cache returning by this method
|
||||
Future<List<File>> list(Account account, File f) async {
|
||||
Future<List<File>?> list(Account account, File f) async {
|
||||
final trimmedRootPath = f.path.trimAny("/");
|
||||
List<File> cache;
|
||||
List<File>? cache;
|
||||
try {
|
||||
cache = await appDbSrc.list(account, f);
|
||||
// compare the cached root
|
||||
|
@ -645,7 +644,7 @@ class _CacheManager {
|
|||
}
|
||||
|
||||
bool get isGood => _isGood;
|
||||
String get remoteTouchToken => _remoteToken;
|
||||
String? get remoteTouchToken => _remoteToken;
|
||||
|
||||
Future<void> _checkTouchToken(
|
||||
Account account, File f, List<File> cache) async {
|
||||
|
@ -653,7 +652,7 @@ class _CacheManager {
|
|||
"${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}";
|
||||
final fileRepo = FileRepo(FileCachedDataSource());
|
||||
final tokenManager = TouchTokenManager();
|
||||
String remoteToken;
|
||||
String? remoteToken;
|
||||
try {
|
||||
remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f);
|
||||
} catch (e, stacktrace) {
|
||||
|
@ -664,7 +663,7 @@ class _CacheManager {
|
|||
}
|
||||
_remoteToken = remoteToken;
|
||||
|
||||
String localToken;
|
||||
String? localToken;
|
||||
try {
|
||||
localToken = await tokenManager.getLocalToken(account, f);
|
||||
} catch (e, stacktrace) {
|
||||
|
@ -687,7 +686,7 @@ class _CacheManager {
|
|||
final bool shouldCheckCache;
|
||||
|
||||
var _isGood = false;
|
||||
String _remoteToken;
|
||||
String? _remoteToken;
|
||||
|
||||
static final _log = Logger("entity.file.data_source._CacheManager");
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class WebdavFileParser {
|
|||
return null;
|
||||
}
|
||||
})
|
||||
.where((element) => element != null)
|
||||
.whereType<File>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
@ -55,19 +55,19 @@ class WebdavFileParser {
|
|||
|
||||
/// Map <DAV:response> contents to File
|
||||
File _toFile(XmlElement element) {
|
||||
String path;
|
||||
int contentLength;
|
||||
String contentType;
|
||||
String etag;
|
||||
DateTime lastModified;
|
||||
bool isCollection;
|
||||
int usedBytes;
|
||||
bool hasPreview;
|
||||
int fileId;
|
||||
String ownerId;
|
||||
Metadata metadata;
|
||||
bool isArchived;
|
||||
DateTime overrideDateTime;
|
||||
String? path;
|
||||
int? contentLength;
|
||||
String? contentType;
|
||||
String? etag;
|
||||
DateTime? lastModified;
|
||||
bool? isCollection;
|
||||
int? usedBytes;
|
||||
bool? hasPreview;
|
||||
int? fileId;
|
||||
String? ownerId;
|
||||
Metadata? metadata;
|
||||
bool? isArchived;
|
||||
DateTime? overrideDateTime;
|
||||
|
||||
for (final child in element.children.whereType<XmlElement>()) {
|
||||
if (child.matchQualifiedName("href",
|
||||
|
@ -105,7 +105,7 @@ class WebdavFileParser {
|
|||
}
|
||||
|
||||
return File(
|
||||
path: path,
|
||||
path: path!,
|
||||
contentLength: contentLength,
|
||||
contentType: contentType,
|
||||
etag: etag,
|
||||
|
@ -140,7 +140,10 @@ class WebdavFileParser {
|
|||
}
|
||||
|
||||
class _PropParser {
|
||||
_PropParser({this.namespaces = const {}, this.logFilePath});
|
||||
_PropParser({
|
||||
this.namespaces = const {},
|
||||
this.logFilePath,
|
||||
});
|
||||
|
||||
/// Parse <DAV:prop> element contents
|
||||
void parse(XmlElement element) {
|
||||
|
@ -201,43 +204,43 @@ class _PropParser {
|
|||
}
|
||||
}
|
||||
|
||||
DateTime get lastModified => _lastModified;
|
||||
int get contentLength => _contentLength;
|
||||
String get contentType => _contentType;
|
||||
String get etag => _etag;
|
||||
int get usedBytes => _usedBytes;
|
||||
bool get isCollection => _isCollection;
|
||||
bool get hasPreview => _hasPreview;
|
||||
int get fileId => _fileId;
|
||||
String get ownerId => _ownerId;
|
||||
Metadata get metadata => _metadata;
|
||||
bool get isArchived => _isArchived;
|
||||
DateTime get overrideDateTime => _overrideDateTime;
|
||||
DateTime? get lastModified => _lastModified;
|
||||
int? get contentLength => _contentLength;
|
||||
String? get contentType => _contentType;
|
||||
String? get etag => _etag;
|
||||
int? get usedBytes => _usedBytes;
|
||||
bool? get isCollection => _isCollection;
|
||||
bool? get hasPreview => _hasPreview;
|
||||
int? get fileId => _fileId;
|
||||
String? get ownerId => _ownerId;
|
||||
Metadata? get metadata => _metadata;
|
||||
bool? get isArchived => _isArchived;
|
||||
DateTime? get overrideDateTime => _overrideDateTime;
|
||||
|
||||
final Map<String, String> namespaces;
|
||||
|
||||
/// File path for logging only
|
||||
final String logFilePath;
|
||||
final String? logFilePath;
|
||||
|
||||
DateTime _lastModified;
|
||||
int _contentLength;
|
||||
String _contentType;
|
||||
String _etag;
|
||||
int _usedBytes;
|
||||
bool _isCollection;
|
||||
bool _hasPreview;
|
||||
int _fileId;
|
||||
String _ownerId;
|
||||
Metadata _metadata;
|
||||
bool _isArchived;
|
||||
DateTime _overrideDateTime;
|
||||
DateTime? _lastModified;
|
||||
int? _contentLength;
|
||||
String? _contentType;
|
||||
String? _etag;
|
||||
int? _usedBytes;
|
||||
bool? _isCollection;
|
||||
bool? _hasPreview;
|
||||
int? _fileId;
|
||||
String? _ownerId;
|
||||
Metadata? _metadata;
|
||||
bool? _isArchived;
|
||||
DateTime? _overrideDateTime;
|
||||
}
|
||||
|
||||
extension on XmlElement {
|
||||
bool matchQualifiedName(
|
||||
String local, {
|
||||
String prefix,
|
||||
Map<String, String> namespaces,
|
||||
required String prefix,
|
||||
required Map<String, String> namespaces,
|
||||
}) {
|
||||
final localNamespaces = <String, String>{};
|
||||
for (final a in attributes) {
|
||||
|
|
|
@ -23,13 +23,13 @@ class AppEventListener<T> {
|
|||
_log.warning("[endListenEvent] Already not listening");
|
||||
return;
|
||||
}
|
||||
_subscription.cancel();
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
}
|
||||
|
||||
final void Function(T) _listener;
|
||||
final _stream = KiwiContainer().resolve<EventBus>().on<T>();
|
||||
StreamSubscription<T> _subscription;
|
||||
StreamSubscription<T>? _subscription;
|
||||
|
||||
final _log = Logger("event.event.AppEventListener<${T.runtimeType}>");
|
||||
}
|
||||
|
|
|
@ -16,7 +16,10 @@ class CacheNotFoundException implements Exception {
|
|||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
ApiException({this.response, this.message});
|
||||
ApiException({
|
||||
required this.response,
|
||||
this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
toString() {
|
||||
|
|
|
@ -10,16 +10,16 @@ import 'package:nc_photos/exception.dart';
|
|||
String toUserString(dynamic exception, BuildContext context) {
|
||||
if (exception is ApiException) {
|
||||
if (exception.response.statusCode == 401) {
|
||||
return AppLocalizations.of(context).errorUnauthenticated;
|
||||
return AppLocalizations.of(context)!.errorUnauthenticated;
|
||||
} else if (exception.response.statusCode == 423) {
|
||||
return AppLocalizations.of(context).errorLocked;
|
||||
return AppLocalizations.of(context)!.errorLocked;
|
||||
} else if (exception.response.statusCode == 500) {
|
||||
return AppLocalizations.of(context).errorServerError;
|
||||
return AppLocalizations.of(context)!.errorServerError;
|
||||
}
|
||||
} else if (exception is SocketException) {
|
||||
return AppLocalizations.of(context).errorDisconnected;
|
||||
return AppLocalizations.of(context)!.errorDisconnected;
|
||||
} else if (exception is InvalidBaseUrlException) {
|
||||
return AppLocalizations.of(context).errorInvalidBaseUrl;
|
||||
return AppLocalizations.of(context)!.errorInvalidBaseUrl;
|
||||
}
|
||||
return exception.toString();
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
extension IterableExtension<T> on Iterable<T> {
|
||||
/// Return a new sorted list
|
||||
List<T> sorted([int compare(T a, T b)]) => this.toList()..sort(compare);
|
||||
List<T> sorted([int compare(T a, T b)?]) => this.toList()..sort(compare);
|
||||
|
||||
/// Return a new stable sorted list
|
||||
List<T> stableSorted([int compare(T a, T b)]) {
|
||||
List<T> stableSorted([int compare(T a, T b)?]) {
|
||||
final tmp = this.toList();
|
||||
mergeSort(tmp, compare: compare);
|
||||
return tmp;
|
||||
|
|
|
@ -6,12 +6,12 @@ class AppLanguage {
|
|||
|
||||
final int langId;
|
||||
final String nativeName;
|
||||
final Locale locale;
|
||||
final Locale? locale;
|
||||
}
|
||||
|
||||
String getSelectedLanguageName(BuildContext context) =>
|
||||
_getSelectedLanguage(context).nativeName;
|
||||
Locale getSelectedLocale(BuildContext context) =>
|
||||
Locale? getSelectedLocale(BuildContext context) =>
|
||||
_getSelectedLanguage(context).locale;
|
||||
|
||||
final supportedLanguages = {
|
||||
|
@ -32,9 +32,9 @@ enum _AppLanguageEnum {
|
|||
|
||||
AppLanguage _getSelectedLanguage(BuildContext context) {
|
||||
try {
|
||||
final lang = Pref.inst().getLanguage();
|
||||
return supportedLanguages[lang];
|
||||
final lang = Pref.inst().getLanguageOr(0);
|
||||
return supportedLanguages[lang]!;
|
||||
} catch (_) {
|
||||
return supportedLanguages[_AppLanguageEnum.systemDefault.index];
|
||||
return supportedLanguages[_AppLanguageEnum.systemDefault.index]!;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,8 +72,8 @@ void _initLog() {
|
|||
|
||||
Future<void> _initPref() async {
|
||||
await Pref.init();
|
||||
if (Pref.inst().getLastVersion(null) == null) {
|
||||
if (Pref.inst().getSetupProgress(null) == null) {
|
||||
if (Pref.inst().getLastVersion() == null) {
|
||||
if (Pref.inst().getSetupProgress() == null) {
|
||||
// new install
|
||||
await Pref.inst().setLastVersion(k.version);
|
||||
} else {
|
||||
|
|
|
@ -25,7 +25,7 @@ class MetadataTask {
|
|||
final op = UpdateMissingMetadata(fileRepo);
|
||||
await for (final _ in op(account,
|
||||
File(path: "${api_util.getWebdavRootUrlRelative(account)}/$r"))) {
|
||||
if (!Pref.inst().isEnableExif()) {
|
||||
if (!Pref.inst().isEnableExifOr()) {
|
||||
_log.info("[call] EXIF disabled, task ending immaturely");
|
||||
op.stop();
|
||||
return;
|
||||
|
@ -53,7 +53,7 @@ class MetadataTaskManager {
|
|||
|
||||
void _handleStream() async {
|
||||
await for (final task in _streamController.stream) {
|
||||
if (Pref.inst().isEnableExif()) {
|
||||
if (Pref.inst().isEnableExifOr()) {
|
||||
_log.info("[_doTask] Executing task: $task");
|
||||
await task();
|
||||
} else {
|
||||
|
|
|
@ -6,11 +6,13 @@ class MediaStore {
|
|||
static const exceptionCodePermissionError = "permissionError";
|
||||
|
||||
static Future<String> saveFileToDownload(
|
||||
String fileName, Uint8List fileContent) =>
|
||||
_channel.invokeMethod("saveFileToDownload", <String, dynamic>{
|
||||
"fileName": fileName,
|
||||
"content": fileContent,
|
||||
});
|
||||
String fileName, Uint8List fileContent) async {
|
||||
return (await _channel
|
||||
.invokeMethod<String>("saveFileToDownload", <String, dynamic>{
|
||||
"fileName": fileName,
|
||||
"content": fileContent,
|
||||
}))!;
|
||||
}
|
||||
|
||||
static const _channel =
|
||||
const MethodChannel("com.nkming.nc_photos/media_store");
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
|
|||
|
||||
class Notification {
|
||||
static Future<void> notifyItemsDownloadSuccessful(
|
||||
List<String> fileUris, List<String> mimeTypes) =>
|
||||
List<String> fileUris, List<String?> mimeTypes) =>
|
||||
_channel.invokeMethod("notifyItemsDownloadSuccessful", <String, dynamic>{
|
||||
"fileUris": fileUris,
|
||||
"mimeTypes": mimeTypes,
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
|
|||
|
||||
class Share {
|
||||
static Future<void> shareItems(
|
||||
List<String> fileUris, List<String> mimeTypes) =>
|
||||
List<String> fileUris, List<String?> mimeTypes) =>
|
||||
_channel.invokeMethod("shareItems", <String, dynamic>{
|
||||
"fileUris": fileUris,
|
||||
"mimeTypes": mimeTypes,
|
||||
|
|
|
@ -4,9 +4,9 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
class Map extends StatelessWidget {
|
||||
const Map({
|
||||
Key key,
|
||||
this.center,
|
||||
this.zoom,
|
||||
Key? key,
|
||||
required this.center,
|
||||
required this.zoom,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -44,5 +44,5 @@ class Map extends StatelessWidget {
|
|||
/// A pair of latitude and longitude coordinates, stored as degrees
|
||||
final Tuple2<double, double> center;
|
||||
final double zoom;
|
||||
final void Function() onTap;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
|
|
@ -11,5 +11,5 @@ class AndroidItemDownloadSuccessfulNotification
|
|||
}
|
||||
|
||||
final List<String> fileUris;
|
||||
final List<String> mimeTypes;
|
||||
final List<String?> mimeTypes;
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ class SelfSignedCertManager {
|
|||
}
|
||||
}
|
||||
|
||||
_BadCertInfo _latestBadCert;
|
||||
late _BadCertInfo _latestBadCert;
|
||||
var _whitelist = <_CertInfo>[];
|
||||
|
||||
static SelfSignedCertManager _inst = SelfSignedCertManager._();
|
||||
|
@ -168,7 +168,7 @@ class _BadCertInfo {
|
|||
|
||||
class _CustomHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext context) {
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
return super.createHttpClient(context)
|
||||
..badCertificateCallback = (cert, host, port) {
|
||||
try {
|
||||
|
|
|
@ -10,5 +10,5 @@ class AndroidShare extends itf.Share {
|
|||
}
|
||||
|
||||
final List<String> fileUris;
|
||||
final List<String> mimeTypes;
|
||||
final List<String?> mimeTypes;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
class OrNull<T> {
|
||||
OrNull(this.obj);
|
||||
|
||||
static bool isNull(OrNull x) => x != null && x.obj == null;
|
||||
/// Return iff the value of [x] is set to null, which means if [x] itself is
|
||||
/// null, false will still be returned
|
||||
static bool isSetNull(OrNull? x) => x != null && x.obj == null;
|
||||
|
||||
final T obj;
|
||||
final T? obj;
|
||||
}
|
||||
|
|
|
@ -9,13 +9,13 @@ abstract class UniversalStorage {
|
|||
|
||||
/// Return the content associated with [name], or null if no such association
|
||||
/// exists
|
||||
Future<Uint8List> getBinary(String name);
|
||||
Future<Uint8List?> getBinary(String name);
|
||||
|
||||
Future<void> putString(String name, String content);
|
||||
|
||||
/// Return the string associated with [name], or null if no such association
|
||||
/// exists
|
||||
Future<String> getString(String name);
|
||||
Future<String?> getString(String name);
|
||||
|
||||
Future<void> remove(String name);
|
||||
}
|
||||
|
|
|
@ -12,67 +12,65 @@ class Pref {
|
|||
|
||||
factory Pref.inst() => _inst;
|
||||
|
||||
List<Account> getAccounts([List<Account> def]) {
|
||||
List<Account>? getAccounts() {
|
||||
final jsonObjs = _pref.getStringList("accounts");
|
||||
return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e)))?.toList() ??
|
||||
def;
|
||||
return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e))).toList();
|
||||
}
|
||||
|
||||
List<Account> getAccountsOr(List<Account> def) => getAccounts() ?? def;
|
||||
Future<bool> setAccounts(List<Account> value) {
|
||||
final jsons = value.map((e) => jsonEncode(e.toJson())).toList();
|
||||
return _pref.setStringList("accounts", jsons);
|
||||
}
|
||||
|
||||
int getCurrentAccountIndex([int def]) =>
|
||||
_pref.getInt("currentAccountIndex") ?? def;
|
||||
|
||||
int? getCurrentAccountIndex() => _pref.getInt("currentAccountIndex");
|
||||
int getCurrentAccountIndexOr(int def) => getCurrentAccountIndex() ?? def;
|
||||
Future<bool> setCurrentAccountIndex(int value) =>
|
||||
_pref.setInt("currentAccountIndex", value);
|
||||
|
||||
int getHomePhotosZoomLevel([int def]) =>
|
||||
_pref.getInt("homePhotosZoomLevel") ?? def;
|
||||
|
||||
int? getHomePhotosZoomLevel() => _pref.getInt("homePhotosZoomLevel");
|
||||
int getHomePhotosZoomLevelOr(int def) => getHomePhotosZoomLevel() ?? def;
|
||||
Future<bool> setHomePhotosZoomLevel(int value) =>
|
||||
_pref.setInt("homePhotosZoomLevel", value);
|
||||
|
||||
int getAlbumViewerZoomLevel([int def]) =>
|
||||
_pref.getInt("albumViewerZoomLevel") ?? def;
|
||||
|
||||
int? getAlbumViewerZoomLevel() => _pref.getInt("albumViewerZoomLevel");
|
||||
int getAlbumViewerZoomLevelOr(int def) => getAlbumViewerZoomLevel() ?? def;
|
||||
Future<bool> setAlbumViewerZoomLevel(int value) =>
|
||||
_pref.setInt("albumViewerZoomLevel", value);
|
||||
|
||||
bool isEnableExif([bool def = true]) => _pref.getBool("isEnableExif") ?? def;
|
||||
|
||||
bool? isEnableExif() => _pref.getBool("isEnableExif");
|
||||
bool isEnableExifOr([bool def = true]) => isEnableExif() ?? def;
|
||||
Future<bool> setEnableExif(bool value) =>
|
||||
_pref.setBool("isEnableExif", value);
|
||||
|
||||
int getSetupProgress([int def = 0]) => _pref.getInt("setupProgress") ?? def;
|
||||
|
||||
int? getSetupProgress() => _pref.getInt("setupProgress");
|
||||
int getSetupProgressOr([int def = 0]) => getSetupProgress() ?? def;
|
||||
Future<bool> setSetupProgress(int value) =>
|
||||
_pref.setInt("setupProgress", value);
|
||||
|
||||
/// Return the version number when the app last ran
|
||||
int getLastVersion([int def = 0]) => _pref.getInt("lastVersion") ?? def;
|
||||
|
||||
int? getLastVersion() => _pref.getInt("lastVersion");
|
||||
int getLastVersionOr(int def) => getLastVersion() ?? def;
|
||||
Future<bool> setLastVersion(int value) => _pref.setInt("lastVersion", value);
|
||||
|
||||
bool isDarkTheme([bool def = false]) => _pref.getBool("isDarkTheme") ?? def;
|
||||
|
||||
bool? isDarkTheme() => _pref.getBool("isDarkTheme");
|
||||
bool isDarkThemeOr(bool def) => isDarkTheme() ?? def;
|
||||
Future<bool> setDarkTheme(bool value) => _pref.setBool("isDarkTheme", value);
|
||||
|
||||
int getLanguage([int def = 0]) => _pref.getInt("language") ?? def;
|
||||
int? getLanguage() => _pref.getInt("language");
|
||||
int getLanguageOr(int def) => getLanguage() ?? def;
|
||||
Future<bool> setLanguage(int value) => _pref.setInt("language", value);
|
||||
|
||||
Pref._();
|
||||
|
||||
static final _inst = Pref._();
|
||||
SharedPreferences _pref;
|
||||
late SharedPreferences _pref;
|
||||
}
|
||||
|
||||
extension PrefExtension on Pref {
|
||||
Account getCurrentAccount() {
|
||||
Account? getCurrentAccount() {
|
||||
try {
|
||||
return Pref.inst().getAccounts()[Pref.inst().getCurrentAccountIndex()];
|
||||
return Pref.inst().getAccounts()![Pref.inst().getCurrentAccountIndex()!];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class ShareHandler {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ProcessingDialog(
|
||||
text: AppLocalizations.of(context).shareDownloadingDialogContent),
|
||||
text: AppLocalizations.of(context)!.shareDownloadingDialogContent),
|
||||
);
|
||||
final results = <Tuple2<File, dynamic>>[];
|
||||
for (final f in files) {
|
||||
|
@ -33,7 +33,7 @@ class ShareHandler {
|
|||
} on PermissionException catch (_) {
|
||||
_log.warning("[shareFiles] Permission not granted");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.downloadFailureNoPermissionNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
|
|
@ -22,7 +22,7 @@ class SnackBarManager {
|
|||
/// Show a snack bar if possible
|
||||
///
|
||||
/// If the snack bar can't be shown at this time, return null
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
|
||||
SnackBar snackBar) {
|
||||
for (final h in _handlers.reversed) {
|
||||
final result = h.showSnackBar(snackBar);
|
||||
|
@ -42,6 +42,6 @@ class SnackBarManager {
|
|||
}
|
||||
|
||||
abstract class SnackBarHandler {
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
|
||||
SnackBar snackBar);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
class AppTheme extends StatelessWidget {
|
||||
const AppTheme({@required this.child});
|
||||
const AppTheme({
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -46,24 +48,24 @@ class AppTheme extends StatelessWidget {
|
|||
|
||||
static Color getSelectionOverlayColor(BuildContext context) {
|
||||
return Theme.of(context).brightness == Brightness.light
|
||||
? primarySwatchLight[100].withOpacity(0.7)
|
||||
: primarySwatchDark[700].withOpacity(0.7);
|
||||
? primarySwatchLight[100]!.withOpacity(0.7)
|
||||
: primarySwatchDark[700]!.withOpacity(0.7);
|
||||
}
|
||||
|
||||
static Color getSelectionCheckColor(BuildContext context) {
|
||||
return Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[800]
|
||||
: Colors.grey[350];
|
||||
? Colors.grey[800]!
|
||||
: Colors.grey[350]!;
|
||||
}
|
||||
|
||||
static Color getOverscrollIndicatorColor(BuildContext context) {
|
||||
return Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[800]
|
||||
: Colors.grey[200];
|
||||
? Colors.grey[800]!
|
||||
: Colors.grey[200]!;
|
||||
}
|
||||
|
||||
static Color getRootPickerContentBoxColor(BuildContext context) {
|
||||
return Colors.blue[200];
|
||||
return Colors.blue[200]!;
|
||||
}
|
||||
|
||||
static Color getPrimaryTextColor(BuildContext context) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import 'package:nc_photos/use_case/remove.dart';
|
|||
/// doing it on every query
|
||||
class TouchTokenManager {
|
||||
Future<void> setRemoteToken(
|
||||
FileRepo fileRepo, Account account, File file, String token) async {
|
||||
FileRepo fileRepo, Account account, File file, String? token) async {
|
||||
_log.info(
|
||||
"[setRemoteToken] Set remote token for file '${file.path}': $token");
|
||||
final path = _getRemotePath(account, file);
|
||||
|
@ -36,7 +36,7 @@ class TouchTokenManager {
|
|||
|
||||
/// Return the touch token for [file] from remote source, or null if no such
|
||||
/// file
|
||||
Future<String> getRemoteToken(
|
||||
Future<String?> getRemoteToken(
|
||||
FileRepo fileRepo, Account account, File file) async {
|
||||
final path = _getRemotePath(account, file);
|
||||
try {
|
||||
|
@ -51,7 +51,7 @@ class TouchTokenManager {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> setLocalToken(Account account, File file, String token) {
|
||||
Future<void> setLocalToken(Account account, File file, String? token) {
|
||||
_log.info(
|
||||
"[setLocalToken] Set local token for file '${file.path}': $token");
|
||||
final name = _getLocalStorageName(account, file);
|
||||
|
@ -62,7 +62,7 @@ class TouchTokenManager {
|
|||
}
|
||||
}
|
||||
|
||||
Future<String> getLocalToken(Account account, File file) async {
|
||||
Future<String?> getLocalToken(Account account, File file) async {
|
||||
final name = _getLocalStorageName(account, file);
|
||||
return platform.UniversalStorage().getString(name);
|
||||
}
|
||||
|
|
|
@ -22,9 +22,9 @@ class LoadMetadata {
|
|||
}
|
||||
|
||||
Future<Map<String, dynamic>> _loadMetadata({
|
||||
@required File file,
|
||||
exifdart.AbstractBlobReader Function() exifdartReaderBuilder,
|
||||
AsyncImageInput Function() imageSizeGetterInputBuilder,
|
||||
required File file,
|
||||
required exifdart.AbstractBlobReader Function() exifdartReaderBuilder,
|
||||
required AsyncImageInput Function() imageSizeGetterInputBuilder,
|
||||
}) async {
|
||||
var metadata = exifdart.Metadata();
|
||||
if (file_util.isMetadataSupportedFormat(file)) {
|
||||
|
@ -47,8 +47,8 @@ class LoadMetadata {
|
|||
await AsyncImageSizeGetter.getSize(imageSizeGetterInputBuilder());
|
||||
// image size getter doesn't handle exif orientation
|
||||
if (metadata.exif?.containsKey("Orientation") == true &&
|
||||
metadata.exif["Orientation"] >= 5 &&
|
||||
metadata.exif["Orientation"] <= 8) {
|
||||
metadata.exif!["Orientation"] >= 5 &&
|
||||
metadata.exif!["Orientation"] <= 8) {
|
||||
// 90 deg CW/CCW
|
||||
imageWidth = resolution.height;
|
||||
imageHeight = resolution.width;
|
||||
|
@ -66,12 +66,12 @@ class LoadMetadata {
|
|||
}
|
||||
} else {
|
||||
if (metadata.rotateAngleCcw != null &&
|
||||
metadata.rotateAngleCcw % 180 != 0) {
|
||||
imageWidth = metadata.imageHeight;
|
||||
imageHeight = metadata.imageWidth;
|
||||
metadata.rotateAngleCcw! % 180 != 0) {
|
||||
imageWidth = metadata.imageHeight!;
|
||||
imageHeight = metadata.imageWidth!;
|
||||
} else {
|
||||
imageWidth = metadata.imageWidth;
|
||||
imageHeight = metadata.imageHeight;
|
||||
imageWidth = metadata.imageWidth!;
|
||||
imageHeight = metadata.imageHeight!;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ class Remove {
|
|||
}
|
||||
|
||||
Future<void> _cleanUpAlbums(Account account, File file) async {
|
||||
final albumRepo = this.albumRepo!;
|
||||
final albums = (await ListAlbum(fileRepo, albumRepo)(account)
|
||||
.where((event) => event is Album)
|
||||
.toList()).cast<Album>();
|
||||
|
@ -58,7 +59,7 @@ class Remove {
|
|||
}
|
||||
|
||||
final FileRepo fileRepo;
|
||||
final AlbumRepo albumRepo;
|
||||
final AlbumRepo? albumRepo;
|
||||
|
||||
static final _log = Logger("use_case.remove.Remove");
|
||||
}
|
||||
|
|
|
@ -44,18 +44,22 @@ class ResyncAlbum {
|
|||
|
||||
Future<AlbumFileItem> _syncOne(Account account, AlbumFileItem item,
|
||||
ObjectStore objStore, Index index) async {
|
||||
Map dbItem;
|
||||
Map? dbItem;
|
||||
if (item.file.fileId != null) {
|
||||
final List dbItems = await index
|
||||
.getAll(AppDbFileDbEntry.toNamespacedFileId(account, item.file));
|
||||
// find the one owned by us
|
||||
dbItem = dbItems.firstWhere((element) {
|
||||
final e = AppDbFileDbEntry.fromJson(element.cast<String, dynamic>());
|
||||
return file_util.getUserDirName(e.file) == account.username;
|
||||
}, orElse: () => null);
|
||||
try {
|
||||
dbItem = dbItems.firstWhere((element) {
|
||||
final e = AppDbFileDbEntry.fromJson(element.cast<String, dynamic>());
|
||||
return file_util.getUserDirName(e.file) == account.username;
|
||||
});
|
||||
} on StateError catch (_) {
|
||||
// not found
|
||||
}
|
||||
} else {
|
||||
dbItem = await objStore
|
||||
.getObject(AppDbFileDbEntry.toPrimaryKey(account, item.file));
|
||||
.getObject(AppDbFileDbEntry.toPrimaryKey(account, item.file)) as Map;
|
||||
}
|
||||
if (dbItem == null) {
|
||||
_log.warning(
|
||||
|
|
|
@ -40,8 +40,9 @@ class UpdateDynamicAlbumCover {
|
|||
}
|
||||
|
||||
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
|
||||
final coverFile = sortedFiles.firstWhere((element) => element.hasPreview);
|
||||
if (coverFile != null) {
|
||||
try {
|
||||
final coverFile =
|
||||
sortedFiles.firstWhere((element) => element.hasPreview ?? false);
|
||||
// cache the result for later use
|
||||
if (coverFile.path !=
|
||||
(album.coverProvider as AlbumAutoCoverProvider).coverFile?.path) {
|
||||
|
@ -51,6 +52,8 @@ class UpdateDynamicAlbumCover {
|
|||
),
|
||||
);
|
||||
}
|
||||
} on StateError catch (_) {
|
||||
// no files
|
||||
}
|
||||
return album;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ class UpdateDynamicAlbumTime {
|
|||
}
|
||||
|
||||
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
|
||||
DateTime latestItemTime;
|
||||
DateTime? latestItemTime;
|
||||
try {
|
||||
latestItemTime = sortedFiles.first.bestDateTime;
|
||||
} catch (_) {
|
||||
|
|
|
@ -40,8 +40,8 @@ class UpdateMissingMetadata {
|
|||
_log.fine("[call] Updating metadata for ${file.path}");
|
||||
final binary = await GetFileBinary(fileRepo)(account, file);
|
||||
final metadata = await LoadMetadata()(account, file, binary);
|
||||
int imageWidth, imageHeight;
|
||||
Exif exif;
|
||||
int? imageWidth, imageHeight;
|
||||
Exif? exif;
|
||||
if (metadata.containsKey("resolution")) {
|
||||
imageWidth = metadata["resolution"]["width"];
|
||||
imageHeight = metadata["resolution"]["height"];
|
||||
|
|
|
@ -12,9 +12,9 @@ class UpdateProperty {
|
|||
Future<void> call(
|
||||
Account account,
|
||||
File file, {
|
||||
OrNull<Metadata> metadata,
|
||||
OrNull<bool> isArchived,
|
||||
OrNull<DateTime> overrideDateTime,
|
||||
OrNull<Metadata>? metadata,
|
||||
OrNull<bool>? isArchived,
|
||||
OrNull<DateTime>? overrideDateTime,
|
||||
}) async {
|
||||
if (metadata == null && isArchived == null && overrideDateTime == null) {
|
||||
// ?
|
||||
|
@ -22,9 +22,9 @@ class UpdateProperty {
|
|||
return;
|
||||
}
|
||||
|
||||
if (metadata?.obj != null && metadata.obj.fileEtag != file.etag) {
|
||||
if (metadata?.obj != null && metadata!.obj!.fileEtag != file.etag) {
|
||||
_log.warning(
|
||||
"[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj.fileEtag}, file: ${file.etag})");
|
||||
"[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj!.fileEtag}, file: ${file.etag})");
|
||||
}
|
||||
await fileRepo.updateProperty(
|
||||
account,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:idb_shim/idb_browser.dart';
|
||||
import 'package:idb_shim/idb_shim.dart';
|
||||
|
||||
IdbFactory getDbFactory() => getIdbFactory();
|
||||
IdbFactory getDbFactory() => idbFactoryBrowser;
|
||||
|
|
|
@ -8,9 +8,9 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
class Map extends StatefulWidget {
|
||||
const Map({
|
||||
Key key,
|
||||
this.center,
|
||||
this.zoom,
|
||||
Key? key,
|
||||
required this.center,
|
||||
required this.zoom,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -20,7 +20,7 @@ class Map extends StatefulWidget {
|
|||
/// A pair of latitude and longitude coordinates, stored as degrees
|
||||
final Tuple2<double, double> center;
|
||||
final double zoom;
|
||||
final void Function() onTap;
|
||||
final void Function()? onTap;
|
||||
}
|
||||
|
||||
class _MapState extends State<Map> {
|
||||
|
|
|
@ -15,8 +15,8 @@ import 'package:nc_photos/widget/sign_in.dart';
|
|||
/// A dialog that allows the user to switch between accounts
|
||||
class AccountPickerDialog extends StatefulWidget {
|
||||
AccountPickerDialog({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -29,7 +29,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_accounts = Pref.inst().getAccounts([]);
|
||||
_accounts = Pref.inst().getAccountsOr([]);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -48,7 +48,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
Icons.close,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
tooltip: AppLocalizations.of(context).deleteTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.deleteTooltip,
|
||||
onPressed: () => _onRemoveItemPressed(a),
|
||||
),
|
||||
),
|
||||
|
@ -63,7 +63,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
..pushNamed(SignIn.routeName);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: AppLocalizations.of(context).addServerTooltip,
|
||||
message: AppLocalizations.of(context)!.addServerTooltip,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
|
@ -89,7 +89,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
Icons.edit,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
tooltip: AppLocalizations.of(context).editTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.editTooltip,
|
||||
onPressed: () => _onEditPressed(),
|
||||
),
|
||||
),
|
||||
|
@ -106,20 +106,28 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
}
|
||||
|
||||
void _onRemoveItemPressed(Account account) {
|
||||
_removeAccount(account);
|
||||
setState(() {
|
||||
_accounts = Pref.inst().getAccounts([]);
|
||||
});
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.removeServerSuccessNotification(account.url)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
try {
|
||||
_removeAccount(account);
|
||||
setState(() {
|
||||
_accounts = Pref.inst().getAccounts()!;
|
||||
});
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.removeServerSuccessNotification(account.url)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} catch (e) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(exception_util.toUserString(e, context)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onEditPressed() async {
|
||||
try {
|
||||
final result = await Navigator.of(context).pushNamed(RootPicker.routeName,
|
||||
final result = await Navigator.of(context).pushNamed<Account>(
|
||||
RootPicker.routeName,
|
||||
arguments: RootPickerArguments(widget.account));
|
||||
if (result != null) {
|
||||
// we've got a good account
|
||||
|
@ -129,19 +137,19 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
final accounts = Pref.inst().getAccounts([]);
|
||||
final accounts = Pref.inst().getAccounts()!;
|
||||
if (accounts.contains(result)) {
|
||||
// conflict with another account. This normally won't happen because
|
||||
// the app passwords are unique to each entry, but just in case
|
||||
Navigator.of(context).pop();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.editAccountConflictFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
accounts[Pref.inst().getCurrentAccountIndex()] = result;
|
||||
accounts[Pref.inst().getCurrentAccountIndex()!] = result;
|
||||
Pref.inst()..setAccounts(accounts);
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context, Home.routeName, (route) => false,
|
||||
|
@ -158,9 +166,9 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
}
|
||||
|
||||
void _removeAccount(Account account) {
|
||||
final currentAccounts = Pref.inst().getAccounts([]);
|
||||
final currentAccounts = Pref.inst().getAccounts()!;
|
||||
final currentAccount =
|
||||
currentAccounts[Pref.inst().getCurrentAccountIndex()];
|
||||
currentAccounts[Pref.inst().getCurrentAccountIndex()!];
|
||||
final newAccounts =
|
||||
currentAccounts.where((element) => element != account).toList();
|
||||
final newAccountIndex = newAccounts.indexOf(currentAccount);
|
||||
|
@ -172,7 +180,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
|||
..setCurrentAccountIndex(newAccountIndex);
|
||||
}
|
||||
|
||||
List<Account> _accounts;
|
||||
late List<Account> _accounts;
|
||||
|
||||
static final _log =
|
||||
Logger("widget.account_picker_dialog._AccountPickerDialogState");
|
||||
|
|
|
@ -21,12 +21,17 @@ class AlbumDirPickerArguments {
|
|||
class AlbumDirPicker extends StatefulWidget {
|
||||
static const routeName = "/album-dir-picker";
|
||||
|
||||
static Route buildRoute(AlbumDirPickerArguments args) =>
|
||||
MaterialPageRoute<List<File>>(
|
||||
builder: (context) => AlbumDirPicker.fromArgs(args),
|
||||
);
|
||||
|
||||
AlbumDirPicker({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
AlbumDirPicker.fromArgs(AlbumDirPickerArguments args, {Key key})
|
||||
AlbumDirPicker.fromArgs(AlbumDirPickerArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -81,7 +86,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
|||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).albumDirPickerHeaderText,
|
||||
AppLocalizations.of(context)!.albumDirPickerHeaderText,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
@ -89,7 +94,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
|||
Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).albumDirPickerSubHeaderText,
|
||||
AppLocalizations.of(context)!.albumDirPickerSubHeaderText,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -112,7 +117,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
|||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => _onConfirmPressed(context),
|
||||
child: Text(AppLocalizations.of(context).confirmButtonLabel),
|
||||
child: Text(AppLocalizations.of(context)!.confirmButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -127,7 +132,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
|||
if (picked.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).albumDirPickerListEmptyNotification),
|
||||
AppLocalizations.of(context)!.albumDirPickerListEmptyNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} else {
|
||||
|
|
|
@ -5,9 +5,9 @@ import 'package:nc_photos/widget/selectable.dart';
|
|||
|
||||
class AlbumGridItem extends StatelessWidget {
|
||||
AlbumGridItem({
|
||||
Key key,
|
||||
@required this.cover,
|
||||
@required this.title,
|
||||
Key? key,
|
||||
required this.cover,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.subtitle2,
|
||||
this.icon,
|
||||
|
@ -50,8 +50,8 @@ class AlbumGridItem extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
title ?? "",
|
||||
style: Theme.of(context).textTheme.bodyText1.copyWith(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyText1!.copyWith(
|
||||
color: AppTheme.getPrimaryTextColor(context),
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
|
@ -64,7 +64,7 @@ class AlbumGridItem extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Text(
|
||||
subtitle ?? "",
|
||||
style: Theme.of(context).textTheme.bodyText2.copyWith(
|
||||
style: Theme.of(context).textTheme.bodyText2!.copyWith(
|
||||
fontSize: 10,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
|
@ -75,8 +75,8 @@ class AlbumGridItem extends StatelessWidget {
|
|||
),
|
||||
if (subtitle2?.isNotEmpty == true)
|
||||
Text(
|
||||
subtitle2,
|
||||
style: Theme.of(context).textTheme.bodyText2.copyWith(
|
||||
subtitle2!,
|
||||
style: Theme.of(context).textTheme.bodyText2!.copyWith(
|
||||
fontSize: 10,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
|
@ -93,12 +93,12 @@ class AlbumGridItem extends StatelessWidget {
|
|||
|
||||
final Widget cover;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String? subtitle;
|
||||
|
||||
/// Appears after [subtitle], aligned to the end side of parent
|
||||
final String subtitle2;
|
||||
final IconData icon;
|
||||
final String? subtitle2;
|
||||
final IconData? icon;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
}
|
||||
|
|
|
@ -34,12 +34,16 @@ class AlbumImporterArguments {
|
|||
class AlbumImporter extends StatefulWidget {
|
||||
static const routeName = "/album-importer";
|
||||
|
||||
static Route buildRoute(AlbumImporterArguments args) => MaterialPageRoute(
|
||||
builder: (context) => AlbumImporter.fromArgs(args),
|
||||
);
|
||||
|
||||
AlbumImporter({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
AlbumImporter.fromArgs(AlbumImporterArguments args, {Key key})
|
||||
AlbumImporter.fromArgs(AlbumImporterArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -98,7 +102,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
|||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).albumImporterHeaderText,
|
||||
AppLocalizations.of(context)!.albumImporterHeaderText,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
@ -106,7 +110,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
|||
Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).albumImporterSubHeaderText,
|
||||
AppLocalizations.of(context)!.albumImporterSubHeaderText,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -141,7 +145,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
|||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => _onImportPressed(context),
|
||||
child: Text(AppLocalizations.of(context).importButtonLabel),
|
||||
child: Text(AppLocalizations.of(context)!.importButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -214,7 +218,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
|||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => ProcessingDialog(
|
||||
text: AppLocalizations.of(context).albumImporterProgressText),
|
||||
text: AppLocalizations.of(context)!.albumImporterProgressText),
|
||||
);
|
||||
try {
|
||||
await _createAllAlbums(context);
|
||||
|
@ -269,7 +273,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
|||
.toList();
|
||||
}
|
||||
|
||||
ListImportableAlbumBloc _bloc;
|
||||
late ListImportableAlbumBloc _bloc;
|
||||
|
||||
var _backingFiles = <File>[];
|
||||
final _picks = <File>[];
|
||||
|
|
|
@ -18,8 +18,8 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
class AlbumPickerDialog extends StatefulWidget {
|
||||
AlbumPickerDialog({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -68,7 +68,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
|
|||
_reqQuery();
|
||||
} else {
|
||||
// process the current state
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_onStateChange(context, _bloc.state);
|
||||
});
|
||||
|
@ -81,7 +81,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
|
|||
SimpleDialogOption(
|
||||
onPressed: () => _onNewAlbumPressed(context),
|
||||
child: Tooltip(
|
||||
message: AppLocalizations.of(context).createAlbumTooltip,
|
||||
message: AppLocalizations.of(context)!.createAlbumTooltip,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
|
@ -170,7 +170,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
|
|||
_bloc.add(ListAlbumBlocQuery(widget.account));
|
||||
}
|
||||
|
||||
ListAlbumBloc _bloc;
|
||||
late ListAlbumBloc _bloc;
|
||||
|
||||
final _items = <Album>[];
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
|||
AlbumSearchDelegate(BuildContext context, this.account)
|
||||
: super(
|
||||
searchFieldLabel:
|
||||
AppLocalizations.of(context).albumSearchTextFieldHint,
|
||||
AppLocalizations.of(context)!.albumSearchTextFieldHint,
|
||||
) {
|
||||
final fileRepo = FileRepo(FileCachedDataSource());
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||
|
@ -38,7 +38,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
|||
return [
|
||||
IconButton(
|
||||
icon: Icon(Icons.clear),
|
||||
tooltip: AppLocalizations.of(context).clearTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.clearTooltip,
|
||||
onPressed: () {
|
||||
query = "";
|
||||
},
|
||||
|
@ -88,7 +88,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context).listNoResultsText,
|
||||
AppLocalizations.of(context)!.listNoResultsText,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -42,13 +42,17 @@ class AlbumViewerArguments {
|
|||
class AlbumViewer extends StatefulWidget {
|
||||
static const routeName = "/album-viewer";
|
||||
|
||||
static Route buildRoute(AlbumViewerArguments args) => MaterialPageRoute(
|
||||
builder: (context) => AlbumViewer.fromArgs(args),
|
||||
);
|
||||
|
||||
AlbumViewer({
|
||||
Key key,
|
||||
@required this.account,
|
||||
@required this.album,
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.album,
|
||||
}) : super(key: key);
|
||||
|
||||
AlbumViewer.fromArgs(AlbumViewerArguments args, {Key key})
|
||||
AlbumViewer.fromArgs(AlbumViewerArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -96,7 +100,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
@override
|
||||
enterEditMode() {
|
||||
super.enterEditMode();
|
||||
_editAlbum = _album.copyWith();
|
||||
_editAlbum = _album!.copyWith();
|
||||
setState(() {
|
||||
_transformItems();
|
||||
});
|
||||
|
@ -104,7 +108,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
if (!SessionStorage().hasShowDragRearrangeNotification) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).albumEditDragRearrangeNotification),
|
||||
AppLocalizations.of(context)!.albumEditDragRearrangeNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
SessionStorage().hasShowDragRearrangeNotification = true;
|
||||
|
@ -112,15 +116,15 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
}
|
||||
|
||||
@override
|
||||
validateEditMode() => _editFormKey?.currentState?.validate() == true;
|
||||
validateEditMode() => _editFormKey.currentState?.validate() == true;
|
||||
|
||||
@override
|
||||
doneEditMode() {
|
||||
try {
|
||||
// persist the changes
|
||||
_editFormKey.currentState.save();
|
||||
final newAlbum = makeEdited(_editAlbum);
|
||||
if (newAlbum.copyWith(lastUpdated: OrNull(_album.lastUpdated)) !=
|
||||
_editFormKey.currentState!.save();
|
||||
final newAlbum = makeEdited(_editAlbum!);
|
||||
if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
|
||||
_album) {
|
||||
_log.info("[doneEditMode] Album modified: $newAlbum");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||
|
@ -217,7 +221,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
} else if (isSelectionMode) {
|
||||
return _buildSelectionAppBar(context);
|
||||
} else {
|
||||
return buildNormalAppBar(context, widget.account, _album);
|
||||
return buildNormalAppBar(context, widget.account, _album!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,14 +230,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
if (platform_k.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarSharePressed(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: AppLocalizations.of(context).removeSelectedFromAlbumTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.removeSelectedFromAlbumTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarRemovePressed();
|
||||
},
|
||||
|
@ -245,12 +249,12 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
return buildEditAppBar(context, widget.account, widget.album, actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.text_fields),
|
||||
tooltip: AppLocalizations.of(context).albumAddTextTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.albumAddTextTooltip,
|
||||
onPressed: _onEditAppBarAddTextPressed,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.sort_by_alpha),
|
||||
tooltip: AppLocalizations.of(context).sortTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.sortTooltip,
|
||||
onPressed: _onEditAppBarSortPressed,
|
||||
),
|
||||
]);
|
||||
|
@ -279,7 +283,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
if (selected.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).shareSelectedEmptyNotification),
|
||||
Text(AppLocalizations.of(context)!.shareSelectedEmptyNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
|
@ -301,14 +305,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
.toList();
|
||||
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||
final newAlbum = _album.copyWith(
|
||||
final newAlbum = _album!.copyWith(
|
||||
provider: AlbumStaticProvider(
|
||||
items: newItems,
|
||||
),
|
||||
);
|
||||
UpdateAlbum(albumRepo)(widget.account, newAlbum).then((_) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.removeSelectedFromAlbumSuccessNotification(
|
||||
selectedIndexes.length)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
|
@ -323,7 +327,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
"${AppLocalizations.of(context).removeSelectedFromAlbumFailureNotification}: "
|
||||
"${AppLocalizations.of(context)!.removeSelectedFromAlbumFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -334,14 +338,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
}
|
||||
|
||||
void _onEditAppBarSortPressed() {
|
||||
final sortProvider = _editAlbum.sortProvider;
|
||||
final sortProvider = _editAlbum!.sortProvider;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FancyOptionPicker(
|
||||
title: AppLocalizations.of(context).sortOptionDialogTitle,
|
||||
title: AppLocalizations.of(context)!.sortOptionDialogTitle,
|
||||
items: [
|
||||
FancyOptionPickerItem(
|
||||
label: AppLocalizations.of(context).sortOptionTimeAscendingLabel,
|
||||
label: AppLocalizations.of(context)!.sortOptionTimeAscendingLabel,
|
||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||
sortProvider.isAscending,
|
||||
onSelect: () {
|
||||
|
@ -350,7 +354,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
},
|
||||
),
|
||||
FancyOptionPickerItem(
|
||||
label: AppLocalizations.of(context).sortOptionTimeDescendingLabel,
|
||||
label: AppLocalizations.of(context)!.sortOptionTimeDescendingLabel,
|
||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||
!sortProvider.isAscending,
|
||||
onSelect: () {
|
||||
|
@ -364,7 +368,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
}
|
||||
|
||||
void _onSortOldestPressed() {
|
||||
_editAlbum = _editAlbum.copyWith(
|
||||
_editAlbum = _editAlbum!.copyWith(
|
||||
sortProvider: AlbumTimeSortProvider(isAscending: true),
|
||||
);
|
||||
setState(() {
|
||||
|
@ -373,7 +377,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
}
|
||||
|
||||
void _onSortNewestPressed() {
|
||||
_editAlbum = _editAlbum.copyWith(
|
||||
_editAlbum = _editAlbum!.copyWith(
|
||||
sortProvider: AlbumTimeSortProvider(isAscending: false),
|
||||
);
|
||||
setState(() {
|
||||
|
@ -431,7 +435,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
final newIndex =
|
||||
toIndex + (isBefore ? 0 : 1) + (fromIndex < toIndex ? -1 : 0);
|
||||
_sortedItems.insert(newIndex, item);
|
||||
_editAlbum = _editAlbum.copyWith(
|
||||
_editAlbum = _editAlbum!.copyWith(
|
||||
sortProvider: AlbumNullSortProvider(),
|
||||
// save the current order
|
||||
provider: AlbumStaticProvider(
|
||||
|
@ -444,14 +448,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
}
|
||||
|
||||
void _onEditAppBarAddTextPressed() {
|
||||
showDialog(
|
||||
showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => SimpleInputDialog(),
|
||||
).then((value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
_editAlbum = _editAlbum.copyWith(
|
||||
_editAlbum = _editAlbum!.copyWith(
|
||||
provider: AlbumStaticProvider(
|
||||
items: [
|
||||
AlbumLabelItem(text: value),
|
||||
|
@ -466,7 +470,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
}
|
||||
|
||||
void _onLabelItemEditPressed(AlbumLabelItem item, int index) {
|
||||
showDialog(
|
||||
showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => SimpleInputDialog(
|
||||
initialText: item.text,
|
||||
|
@ -476,7 +480,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
return;
|
||||
}
|
||||
_sortedItems[index] = AlbumLabelItem(text: value);
|
||||
_editAlbum = _editAlbum.copyWith(
|
||||
_editAlbum = _editAlbum!.copyWith(
|
||||
provider: AlbumStaticProvider(
|
||||
items: _sortedItems,
|
||||
),
|
||||
|
@ -490,9 +494,10 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
void _transformItems() {
|
||||
if (_editAlbum != null) {
|
||||
// edit mode
|
||||
_sortedItems = _editAlbum.sortProvider.sort(_getAlbumItemsOf(_editAlbum));
|
||||
_sortedItems =
|
||||
_editAlbum!.sortProvider.sort(_getAlbumItemsOf(_editAlbum!));
|
||||
} else {
|
||||
_sortedItems = _album.sortProvider.sort(_getAlbumItemsOf(_album));
|
||||
_sortedItems = _album!.sortProvider.sort(_getAlbumItemsOf(_album!));
|
||||
}
|
||||
_backingFiles = _sortedItems
|
||||
.whereType<AlbumFileItem>()
|
||||
|
@ -644,29 +649,29 @@ class _AlbumViewerState extends State<AlbumViewer>
|
|||
static List<AlbumItem> _getAlbumItemsOf(Album a) =>
|
||||
AlbumStaticProvider.of(a).items;
|
||||
|
||||
Album _album;
|
||||
Album? _album;
|
||||
var _sortedItems = <AlbumItem>[];
|
||||
var _backingFiles = <File>[];
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
double _itemListMaxExtent;
|
||||
double? _itemListMaxExtent;
|
||||
bool _isDragging = false;
|
||||
// == null if not drag scrolling
|
||||
bool _isDragScrollingDown;
|
||||
bool? _isDragScrollingDown;
|
||||
final _editFormKey = GlobalKey<FormState>();
|
||||
Album _editAlbum;
|
||||
Album? _editAlbum;
|
||||
|
||||
static final _log = Logger("widget.album_viewer._AlbumViewerState");
|
||||
}
|
||||
|
||||
abstract class _ListItem implements SelectableItem, DraggableItem {
|
||||
_ListItem({
|
||||
@required this.index,
|
||||
VoidCallback onTap,
|
||||
DragTargetAccept<DraggableItem> onDropBefore,
|
||||
DragTargetAccept<DraggableItem> onDropAfter,
|
||||
VoidCallback onDragStarted,
|
||||
VoidCallback onDragEndedAny,
|
||||
required this.index,
|
||||
VoidCallback? onTap,
|
||||
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||
VoidCallback? onDragStarted,
|
||||
VoidCallback? onDragEndedAny,
|
||||
}) : _onTap = onTap,
|
||||
_onDropBefore = onDropBefore,
|
||||
_onDropAfter = onDropAfter,
|
||||
|
@ -709,22 +714,22 @@ abstract class _ListItem implements SelectableItem, DraggableItem {
|
|||
|
||||
final int index;
|
||||
|
||||
final VoidCallback _onTap;
|
||||
final DragTargetAccept<DraggableItem> _onDropBefore;
|
||||
final DragTargetAccept<DraggableItem> _onDropAfter;
|
||||
final VoidCallback _onDragStarted;
|
||||
final VoidCallback _onDragEndedAny;
|
||||
final VoidCallback? _onTap;
|
||||
final DragTargetAccept<DraggableItem>? _onDropBefore;
|
||||
final DragTargetAccept<DraggableItem>? _onDropAfter;
|
||||
final VoidCallback? _onDragStarted;
|
||||
final VoidCallback? _onDragEndedAny;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
@required int index,
|
||||
@required this.file,
|
||||
VoidCallback onTap,
|
||||
DragTargetAccept<DraggableItem> onDropBefore,
|
||||
DragTargetAccept<DraggableItem> onDropAfter,
|
||||
VoidCallback onDragStarted,
|
||||
VoidCallback onDragEndedAny,
|
||||
required int index,
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||
VoidCallback? onDragStarted,
|
||||
VoidCallback? onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
onTap: onTap,
|
||||
|
@ -739,15 +744,15 @@ abstract class _FileListItem extends _ListItem {
|
|||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
@required int index,
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
DragTargetAccept<DraggableItem> onDropBefore,
|
||||
DragTargetAccept<DraggableItem> onDropAfter,
|
||||
VoidCallback onDragStarted,
|
||||
VoidCallback onDragEndedAny,
|
||||
required int index,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||
VoidCallback? onDragStarted,
|
||||
VoidCallback? onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
file: file,
|
||||
|
@ -773,15 +778,15 @@ class _ImageListItem extends _FileListItem {
|
|||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
@required int index,
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
DragTargetAccept<DraggableItem> onDropBefore,
|
||||
DragTargetAccept<DraggableItem> onDropAfter,
|
||||
VoidCallback onDragStarted,
|
||||
VoidCallback onDragEndedAny,
|
||||
required int index,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||
VoidCallback? onDragStarted,
|
||||
VoidCallback? onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
file: file,
|
||||
|
@ -806,12 +811,12 @@ class _VideoListItem extends _FileListItem {
|
|||
|
||||
class _LabelListItem extends _ListItem {
|
||||
_LabelListItem({
|
||||
@required int index,
|
||||
@required this.text,
|
||||
DragTargetAccept<DraggableItem> onDropBefore,
|
||||
DragTargetAccept<DraggableItem> onDropAfter,
|
||||
VoidCallback onDragStarted,
|
||||
VoidCallback onDragEndedAny,
|
||||
required int index,
|
||||
required this.text,
|
||||
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||
VoidCallback? onDragStarted,
|
||||
VoidCallback? onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
onDropBefore: onDropBefore,
|
||||
|
@ -842,13 +847,13 @@ class _LabelListItem extends _ListItem {
|
|||
|
||||
class _EditLabelListItem extends _LabelListItem {
|
||||
_EditLabelListItem({
|
||||
@required int index,
|
||||
@required String text,
|
||||
@required this.onEditPressed,
|
||||
DragTargetAccept<DraggableItem> onDropBefore,
|
||||
DragTargetAccept<DraggableItem> onDropAfter,
|
||||
VoidCallback onDragStarted,
|
||||
VoidCallback onDragEndedAny,
|
||||
required int index,
|
||||
required String text,
|
||||
required this.onEditPressed,
|
||||
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||
VoidCallback? onDragStarted,
|
||||
VoidCallback? onDragEndedAny,
|
||||
}) : super(
|
||||
index: index,
|
||||
text: text,
|
||||
|
@ -873,7 +878,7 @@ class _EditLabelListItem extends _LabelListItem {
|
|||
end: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.edit),
|
||||
tooltip: AppLocalizations.of(context).editTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.editTooltip,
|
||||
onPressed: onEditPressed,
|
||||
),
|
||||
),
|
||||
|
@ -886,5 +891,5 @@ class _EditLabelListItem extends _LabelListItem {
|
|||
return super.buildWidget(context);
|
||||
}
|
||||
|
||||
final VoidCallback onEditPressed;
|
||||
final VoidCallback? onEditPressed;
|
||||
}
|
||||
|
|
|
@ -18,20 +18,17 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0);
|
||||
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevelOr(0);
|
||||
}
|
||||
|
||||
@protected
|
||||
File initCover(Account account, List<File> backingFiles) {
|
||||
void initCover(Account account, List<File> backingFiles) {
|
||||
try {
|
||||
final coverFile =
|
||||
backingFiles.firstWhere((element) => element.hasPreview);
|
||||
backingFiles.firstWhere((element) => element.hasPreview ?? false);
|
||||
_coverPreviewUrl = api_util.getFilePreviewUrl(account, coverFile,
|
||||
width: 1024, height: 600);
|
||||
return coverFile;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@protected
|
||||
|
@ -39,9 +36,9 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
BuildContext context,
|
||||
Account account,
|
||||
Album album, {
|
||||
List<Widget> actions,
|
||||
List<PopupMenuEntry<int>> Function(BuildContext) menuItemBuilder,
|
||||
void Function(int) onSelectedMenuItem,
|
||||
List<Widget>? actions,
|
||||
List<PopupMenuEntry<int>> Function(BuildContext)? menuItemBuilder,
|
||||
void Function(int)? onSelectedMenuItem,
|
||||
}) {
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
|
@ -58,7 +55,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.photo_size_select_large),
|
||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.zoomTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuZoom(
|
||||
initialValue: _thumbZoomLevel,
|
||||
|
@ -74,12 +71,12 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
],
|
||||
),
|
||||
...(actions ?? []),
|
||||
PopupMenuButton(
|
||||
PopupMenuButton<int>(
|
||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: -1,
|
||||
child: Text(AppLocalizations.of(context).editAlbumMenuLabel),
|
||||
child: Text(AppLocalizations.of(context)!.editAlbumMenuLabel),
|
||||
),
|
||||
...(menuItemBuilder?.call(context) ?? []),
|
||||
],
|
||||
|
@ -120,7 +117,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
});
|
||||
},
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.selectionAppBarTitle(selectedListItems.length)),
|
||||
actions: actions,
|
||||
),
|
||||
|
@ -132,7 +129,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
BuildContext context,
|
||||
Account account,
|
||||
Album album, {
|
||||
List<Widget> actions,
|
||||
List<Widget>? actions,
|
||||
}) {
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
|
@ -141,16 +138,17 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
background: _getAppBarCover(context, account),
|
||||
title: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context).nameInputHint,
|
||||
hintText: AppLocalizations.of(context)!.nameInputHint,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return AppLocalizations.of(context).albumNameInputInvalidEmpty;
|
||||
if (value?.isNotEmpty == true) {
|
||||
return null;
|
||||
} else {
|
||||
return AppLocalizations.of(context)!.albumNameInputInvalidEmpty;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_editFormValue.name = value;
|
||||
_editFormValue.name = value!;
|
||||
},
|
||||
onChanged: (value) {
|
||||
// need to save the value otherwise it'll return to the initial
|
||||
|
@ -166,7 +164,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
leading: IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
tooltip: AppLocalizations.of(context).doneButtonTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.doneButtonTooltip,
|
||||
onPressed: () {
|
||||
if (validateEditMode()) {
|
||||
setState(() {
|
||||
|
@ -226,7 +224,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
});
|
||||
}
|
||||
|
||||
Widget _getAppBarCover(BuildContext context, Account account) {
|
||||
Widget? _getAppBarCover(BuildContext context, Account account) {
|
||||
try {
|
||||
if (_coverPreviewUrl != null) {
|
||||
return Opacity(
|
||||
|
@ -236,7 +234,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
clipBehavior: Clip.hardEdge,
|
||||
fit: BoxFit.cover,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: _coverPreviewUrl,
|
||||
imageUrl: _coverPreviewUrl!,
|
||||
httpHeaders: {
|
||||
"Authorization": Api.getAuthorizationHeaderValue(account),
|
||||
},
|
||||
|
@ -254,11 +252,11 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
return null;
|
||||
}
|
||||
|
||||
String _coverPreviewUrl;
|
||||
String? _coverPreviewUrl;
|
||||
var _thumbZoomLevel = 0;
|
||||
|
||||
var _isEditMode = false;
|
||||
String _editNameValue;
|
||||
String? _editNameValue;
|
||||
var _editFormValue = _EditFormValue();
|
||||
|
||||
static final _log = Logger("widget.album_viewer_mixin.AlbumViewerMixin");
|
||||
|
@ -266,5 +264,5 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
|||
}
|
||||
|
||||
class _EditFormValue {
|
||||
String name;
|
||||
late String name;
|
||||
}
|
||||
|
|
|
@ -5,14 +5,14 @@ import 'package:flutter/widgets.dart';
|
|||
/// The point is to disable non-visible buttons
|
||||
class AnimatedVisibility extends StatefulWidget {
|
||||
const AnimatedVisibility({
|
||||
Key key,
|
||||
this.child,
|
||||
@required this.opacity,
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.opacity,
|
||||
this.curve = Curves.linear,
|
||||
@required this.duration,
|
||||
required this.duration,
|
||||
this.onEnd,
|
||||
this.alwaysIncludeSemantics = false,
|
||||
}) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
|
||||
}) : assert(opacity >= 0.0 && opacity <= 1.0),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -22,7 +22,7 @@ class AnimatedVisibility extends StatefulWidget {
|
|||
final double opacity;
|
||||
final Curve curve;
|
||||
final Duration duration;
|
||||
final VoidCallback onEnd;
|
||||
final VoidCallback? onEnd;
|
||||
final bool alwaysIncludeSemantics;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,12 +32,16 @@ class ArchiveViewerArguments {
|
|||
class ArchiveViewer extends StatefulWidget {
|
||||
static const routeName = "/archive-viewer";
|
||||
|
||||
static Route buildRoute(ArchiveViewerArguments args) => MaterialPageRoute(
|
||||
builder: (context) => ArchiveViewer.fromArgs(args),
|
||||
);
|
||||
|
||||
ArchiveViewer({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
ArchiveViewer.fromArgs(ArchiveViewerArguments args, {Key key})
|
||||
ArchiveViewer.fromArgs(ArchiveViewerArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -55,7 +59,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
initState() {
|
||||
super.initState();
|
||||
_initBloc();
|
||||
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0);
|
||||
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevelOr(0);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -81,7 +85,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
_reqQuery();
|
||||
} else {
|
||||
// process the current state
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_onStateChange(context, _bloc.state);
|
||||
});
|
||||
|
@ -144,12 +148,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
});
|
||||
},
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.selectionAppBarTitle(selectedListItems.length)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.unarchive),
|
||||
tooltip: AppLocalizations.of(context).unarchiveSelectedTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.unarchiveSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarUnarchivePressed();
|
||||
},
|
||||
|
@ -161,12 +165,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
|
||||
Widget _buildNormalAppBar(BuildContext context) {
|
||||
return SliverAppBar(
|
||||
title: Text(AppLocalizations.of(context).albumArchiveLabel),
|
||||
title: Text(AppLocalizations.of(context)!.albumArchiveLabel),
|
||||
floating: true,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.photo_size_select_large),
|
||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.zoomTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuZoom(
|
||||
initialValue: _thumbZoomLevel,
|
||||
|
@ -208,7 +212,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
|
||||
Future<void> _onSelectionAppBarUnarchivePressed() async {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.unarchiveSelectedProcessingNotification(selectedListItems.length)),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
|
@ -237,12 +241,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
if (failures.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).unarchiveSelectedSuccessNotification),
|
||||
AppLocalizations.of(context)!.unarchiveSelectedSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} else {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.unarchiveSelectedFailureNotification(failures.length)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -308,7 +312,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
}
|
||||
}
|
||||
|
||||
ScanDirBloc _bloc;
|
||||
late ScanDirBloc _bloc;
|
||||
|
||||
var _backingFiles = <File>[];
|
||||
|
||||
|
@ -319,7 +323,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
|||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
VoidCallback onTap,
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
|
@ -331,13 +335,13 @@ abstract class _ListItem implements SelectableItem {
|
|||
@override
|
||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
final VoidCallback _onTap;
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
@required this.file,
|
||||
VoidCallback onTap,
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
@override
|
||||
|
@ -353,10 +357,10 @@ abstract class _FileListItem extends _ListItem {
|
|||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
|
@ -374,10 +378,10 @@ class _ImageListItem extends _FileListItem {
|
|||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
|
|
|
@ -13,8 +13,8 @@ import 'package:nc_photos/widget/album_grid_item.dart';
|
|||
/// Build a standard [AlbumGridItem] for an [Album]
|
||||
class AlbumGridItemBuilder {
|
||||
AlbumGridItemBuilder({
|
||||
@required this.account,
|
||||
@required this.album,
|
||||
required this.account,
|
||||
required this.album,
|
||||
this.isSelected = false,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
|
@ -22,9 +22,9 @@ class AlbumGridItemBuilder {
|
|||
|
||||
AlbumGridItem build(BuildContext context) {
|
||||
var subtitle = "";
|
||||
String subtitle2;
|
||||
String? subtitle2;
|
||||
if (album.provider is AlbumStaticProvider) {
|
||||
subtitle = AppLocalizations.of(context)
|
||||
subtitle = AppLocalizations.of(context)!
|
||||
.albumSize(AlbumStaticProvider.of(album).items.length);
|
||||
} else if (album.provider is AlbumDirProvider) {
|
||||
final provider = album.provider as AlbumDirProvider;
|
||||
|
@ -49,7 +49,7 @@ class AlbumGridItemBuilder {
|
|||
Widget cover;
|
||||
try {
|
||||
final coverFile = album.coverProvider.getCover(album);
|
||||
final previewUrl = api_util.getFilePreviewUrl(account, coverFile,
|
||||
final previewUrl = api_util.getFilePreviewUrl(account, coverFile!,
|
||||
width: 512, height: 512);
|
||||
cover = FittedBox(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
|
@ -89,6 +89,6 @@ class AlbumGridItemBuilder {
|
|||
final Account account;
|
||||
final Album album;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
/// to clear the image from the [ImageCache].
|
||||
static Future evictFromCache(
|
||||
String url, {
|
||||
String cacheKey,
|
||||
BaseCacheManager cacheManager,
|
||||
String? cacheKey,
|
||||
BaseCacheManager? cacheManager,
|
||||
double scale = 1.0,
|
||||
}) async {
|
||||
cacheManager = cacheManager ?? DefaultCacheManager();
|
||||
|
@ -33,31 +33,31 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
final CachedNetworkImageProvider _image;
|
||||
|
||||
/// Option to use cachemanager with other settings
|
||||
final BaseCacheManager cacheManager;
|
||||
final BaseCacheManager? cacheManager;
|
||||
|
||||
/// The target image that is displayed.
|
||||
final String imageUrl;
|
||||
|
||||
/// The target image's cache key.
|
||||
final String cacheKey;
|
||||
final String? cacheKey;
|
||||
|
||||
/// Optional builder to further customize the display of the image.
|
||||
final ImageWidgetBuilder imageBuilder;
|
||||
final ImageWidgetBuilder? imageBuilder;
|
||||
|
||||
/// Widget displayed while the target [imageUrl] is loading.
|
||||
final PlaceholderWidgetBuilder placeholder;
|
||||
final PlaceholderWidgetBuilder? placeholder;
|
||||
|
||||
/// Widget displayed while the target [imageUrl] is loading.
|
||||
final ProgressIndicatorBuilder progressIndicatorBuilder;
|
||||
final ProgressIndicatorBuilder? progressIndicatorBuilder;
|
||||
|
||||
/// Widget displayed while the target [imageUrl] failed loading.
|
||||
final LoadingErrorWidgetBuilder errorWidget;
|
||||
final LoadingErrorWidgetBuilder? errorWidget;
|
||||
|
||||
/// The duration of the fade-in animation for the [placeholder].
|
||||
final Duration placeholderFadeInDuration;
|
||||
final Duration? placeholderFadeInDuration;
|
||||
|
||||
/// The duration of the fade-out animation for the [placeholder].
|
||||
final Duration fadeOutDuration;
|
||||
final Duration? fadeOutDuration;
|
||||
|
||||
/// The curve of the fade-out animation for the [placeholder].
|
||||
final Curve fadeOutCurve;
|
||||
|
@ -74,7 +74,7 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
/// aspect ratio. This may result in a sudden change if the size of the
|
||||
/// placeholder widget does not match that of the target image. The size is
|
||||
/// also affected by the scale factor.
|
||||
final double width;
|
||||
final double? width;
|
||||
|
||||
/// If non-null, require the image to have this height.
|
||||
///
|
||||
|
@ -82,13 +82,13 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
/// aspect ratio. This may result in a sudden change if the size of the
|
||||
/// placeholder widget does not match that of the target image. The size is
|
||||
/// also affected by the scale factor.
|
||||
final double height;
|
||||
final double? height;
|
||||
|
||||
/// How to inscribe the image into the space allocated during layout.
|
||||
///
|
||||
/// The default varies based on the other fields. See the discussion at
|
||||
/// [paintImage].
|
||||
final BoxFit fit;
|
||||
final BoxFit? fit;
|
||||
|
||||
/// How to align the image within its bounds.
|
||||
///
|
||||
|
@ -112,7 +112,7 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
/// specify an [AlignmentGeometry].
|
||||
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
|
||||
/// relative to text direction.
|
||||
final AlignmentGeometry alignment;
|
||||
final Alignment alignment;
|
||||
|
||||
/// How to paint any portions of the layout bounds not covered by the image.
|
||||
final ImageRepeat repeat;
|
||||
|
@ -135,14 +135,14 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
final bool matchTextDirection;
|
||||
|
||||
/// Optional headers for the http request of the image url
|
||||
final Map<String, String> httpHeaders;
|
||||
final Map<String, String>? httpHeaders;
|
||||
|
||||
/// When set to true it will animate from the old image to the new image
|
||||
/// if the url changes.
|
||||
final bool useOldImageOnUrlChange;
|
||||
|
||||
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
|
||||
final Color color;
|
||||
final Color? color;
|
||||
|
||||
/// Used to combine [color] with this image.
|
||||
///
|
||||
|
@ -152,7 +152,7 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
/// See also:
|
||||
///
|
||||
/// * [BlendMode], which includes an illustration of the effect of each blend mode.
|
||||
final BlendMode colorBlendMode;
|
||||
final BlendMode? colorBlendMode;
|
||||
|
||||
/// Target the interpolation quality for image scaling.
|
||||
///
|
||||
|
@ -160,24 +160,24 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
final FilterQuality filterQuality;
|
||||
|
||||
/// Will resize the image in memory to have a certain width using [ResizeImage]
|
||||
final int memCacheWidth;
|
||||
final int? memCacheWidth;
|
||||
|
||||
/// Will resize the image in memory to have a certain height using [ResizeImage]
|
||||
final int memCacheHeight;
|
||||
final int? memCacheHeight;
|
||||
|
||||
/// Will resize the image and store the resized image in the disk cache.
|
||||
final int maxWidthDiskCache;
|
||||
final int? maxWidthDiskCache;
|
||||
|
||||
/// Will resize the image and store the resized image in the disk cache.
|
||||
final int maxHeightDiskCache;
|
||||
final int? maxHeightDiskCache;
|
||||
|
||||
/// CachedNetworkImage shows a network image using a caching mechanism. It also
|
||||
/// provides support for a placeholder, showing an error and fading into the
|
||||
/// loaded image. Next to that it supports most features of a default Image
|
||||
/// widget.
|
||||
CachedNetworkImage({
|
||||
Key key,
|
||||
@required this.imageUrl,
|
||||
Key? key,
|
||||
required this.imageUrl,
|
||||
this.httpHeaders,
|
||||
this.imageBuilder,
|
||||
this.placeholder,
|
||||
|
@ -204,17 +204,8 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
this.cacheKey,
|
||||
this.maxWidthDiskCache,
|
||||
this.maxHeightDiskCache,
|
||||
ImageRenderMethodForWeb imageRenderMethodForWeb,
|
||||
}) : assert(imageUrl != null),
|
||||
assert(fadeOutDuration != null),
|
||||
assert(fadeOutCurve != null),
|
||||
assert(fadeInDuration != null),
|
||||
assert(fadeInCurve != null),
|
||||
assert(alignment != null),
|
||||
assert(filterQuality != null),
|
||||
assert(repeat != null),
|
||||
assert(matchTextDirection != null),
|
||||
_image = CachedNetworkImageProvider(
|
||||
ImageRenderMethodForWeb? imageRenderMethodForWeb,
|
||||
}) : _image = CachedNetworkImageProvider(
|
||||
imageUrl,
|
||||
headers: httpHeaders,
|
||||
cacheManager: cacheManager,
|
||||
|
@ -267,32 +258,32 @@ class CachedNetworkImage extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget _octoImageBuilder(BuildContext context, Widget child) {
|
||||
return imageBuilder(context, child, _image);
|
||||
return imageBuilder!(context, child, _image);
|
||||
}
|
||||
|
||||
Widget _octoPlaceholderBuilder(BuildContext context) {
|
||||
return placeholder(context, imageUrl);
|
||||
return placeholder!(context, imageUrl);
|
||||
}
|
||||
|
||||
Widget _octoProgressIndicatorBuilder(
|
||||
BuildContext context,
|
||||
ImageChunkEvent progress,
|
||||
ImageChunkEvent? progress,
|
||||
) {
|
||||
int totalSize;
|
||||
int? totalSize;
|
||||
var downloaded = 0;
|
||||
if (progress != null) {
|
||||
totalSize = progress.expectedTotalBytes;
|
||||
downloaded = progress.cumulativeBytesLoaded;
|
||||
}
|
||||
return progressIndicatorBuilder(
|
||||
return progressIndicatorBuilder!(
|
||||
context, imageUrl, DownloadProgress(imageUrl, totalSize, downloaded));
|
||||
}
|
||||
|
||||
Widget _octoErrorBuilder(
|
||||
BuildContext context,
|
||||
Object error,
|
||||
StackTrace stackTrace,
|
||||
StackTrace? stackTrace,
|
||||
) {
|
||||
return errorWidget(context, imageUrl, error);
|
||||
return errorWidget!(context, imageUrl, error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,12 +25,16 @@ class ConnectArguments {
|
|||
class Connect extends StatefulWidget {
|
||||
static const routeName = "/connect";
|
||||
|
||||
static Route buildRoute(ConnectArguments args) => MaterialPageRoute<Account>(
|
||||
builder: (context) => Connect.fromArgs(args),
|
||||
);
|
||||
|
||||
Connect({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
Connect.fromArgs(ConnectArguments args, {Key key})
|
||||
Connect.fromArgs(ConnectArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -82,7 +86,7 @@ class _ConnectState extends State<Connect> {
|
|||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)
|
||||
AppLocalizations.of(context)!
|
||||
.connectingToServer(widget.account.url),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
|
@ -106,7 +110,7 @@ class _ConnectState extends State<Connect> {
|
|||
} else if (state.exception is ApiException &&
|
||||
(state.exception as ApiException).response.statusCode == 401) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context).errorWrongPassword),
|
||||
content: Text(AppLocalizations.of(context)!.errorWrongPassword),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
Navigator.of(context).pop(null);
|
||||
|
@ -124,9 +128,9 @@ class _ConnectState extends State<Connect> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).serverCertErrorDialogTitle),
|
||||
title: Text(AppLocalizations.of(context)!.serverCertErrorDialogTitle),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).serverCertErrorDialogContent),
|
||||
Text(AppLocalizations.of(context)!.serverCertErrorDialogContent),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
|
@ -138,7 +142,7 @@ class _ConnectState extends State<Connect> {
|
|||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).advancedButtonLabel),
|
||||
child: Text(AppLocalizations.of(context)!.advancedButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -150,10 +154,11 @@ class _ConnectState extends State<Connect> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).whitelistCertDialogTitle),
|
||||
content: Text(AppLocalizations.of(context).whitelistCertDialogContent(
|
||||
SelfSignedCertManager().getLastBadCertHost(),
|
||||
SelfSignedCertManager().getLastBadCertFingerprint())),
|
||||
title: Text(AppLocalizations.of(context)!.whitelistCertDialogTitle),
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.whitelistCertDialogContent(
|
||||
SelfSignedCertManager().getLastBadCertHost(),
|
||||
SelfSignedCertManager().getLastBadCertFingerprint())),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
|
@ -166,7 +171,7 @@ class _ConnectState extends State<Connect> {
|
|||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child:
|
||||
Text(AppLocalizations.of(context).whitelistCertButtonLabel),
|
||||
Text(AppLocalizations.of(context)!.whitelistCertButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -100,7 +100,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
dense: true,
|
||||
leading: const SizedBox(width: 24),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).rootPickerNavigateUpItemText),
|
||||
AppLocalizations.of(context)!.rootPickerNavigateUpItemText),
|
||||
onTap: () {
|
||||
try {
|
||||
_navigateInto(File(path: path.dirname(_currentPath)));
|
||||
|
@ -128,7 +128,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
final canPick = canPickDir(item.file);
|
||||
final pickState = _isItemPicked(item);
|
||||
|
||||
IconData iconData;
|
||||
IconData? iconData;
|
||||
if (canPick) {
|
||||
switch (pickState) {
|
||||
case _PickState.picked:
|
||||
|
@ -170,9 +170,10 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
onPressed: null,
|
||||
),
|
||||
title: Text(path.basename(item.file.path)),
|
||||
trailing:
|
||||
item.children.isNotEmpty ? const Icon(Icons.arrow_forward_ios) : null,
|
||||
onTap: item.children.isNotEmpty
|
||||
trailing: item.children?.isNotEmpty == true
|
||||
? const Icon(Icons.arrow_forward_ios)
|
||||
: null,
|
||||
onTap: item.children?.isNotEmpty == true
|
||||
? () {
|
||||
try {
|
||||
_navigateInto(item.file);
|
||||
|
@ -238,12 +239,12 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
// this dir is explicitly picked, nothing more to do
|
||||
return [item.file];
|
||||
}
|
||||
if (item.children == null || item.children.isEmpty) {
|
||||
if (item.children == null || item.children!.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final products = <File>[];
|
||||
for (final i in item.children) {
|
||||
for (final i in item.children!) {
|
||||
products.addAll(_optimizePicks(i));
|
||||
}
|
||||
// // see if all children are being picked
|
||||
|
@ -282,7 +283,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
_picks.removeWhere((element) => identical(element, parent));
|
||||
} catch (_) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.rootPickerUnpickFailureNotification)));
|
||||
}
|
||||
}
|
||||
|
@ -296,12 +297,13 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
/// Either [path] or [item] must be set, If both are set, [item] takes
|
||||
/// priority
|
||||
List<LsDirBlocItem> _pickedAllExclude({
|
||||
String path,
|
||||
LsDirBlocItem item,
|
||||
@required LsDirBlocItem exclude,
|
||||
String? path,
|
||||
LsDirBlocItem? item,
|
||||
required LsDirBlocItem exclude,
|
||||
}) {
|
||||
assert(path != null || item != null);
|
||||
if (item == null) {
|
||||
final item = _findChildItemByPath(_root, path);
|
||||
final item = _findChildItemByPath(_root, path!);
|
||||
return _pickedAllExclude(item: item, exclude: exclude);
|
||||
}
|
||||
|
||||
|
@ -311,7 +313,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
_log.fine(
|
||||
"[_pickedAllExclude] Unpicking '${item.file.path}' and picking children");
|
||||
final products = <LsDirBlocItem>[];
|
||||
for (final i in item.children) {
|
||||
for (final i in item.children ?? []) {
|
||||
if (exclude.file.path.startsWith(i.file.path)) {
|
||||
// [i] is a parent of exclude
|
||||
products.addAll(_pickedAllExclude(item: i, exclude: exclude));
|
||||
|
@ -327,7 +329,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
if (path == parent.file.path) {
|
||||
return parent;
|
||||
}
|
||||
for (final c in parent.children) {
|
||||
for (final c in parent.children ?? []) {
|
||||
if (path == c.file.path || path.startsWith("${c.file.path}/")) {
|
||||
return _findChildItemByPath(c, path);
|
||||
}
|
||||
|
@ -364,11 +366,11 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
|||
_bloc.add(LsDirBlocQuery(getAccount(), file, depth: 2));
|
||||
}
|
||||
|
||||
LsDirBloc _bloc;
|
||||
LsDirBlocItem _root;
|
||||
late LsDirBloc _bloc;
|
||||
late LsDirBlocItem _root;
|
||||
|
||||
/// Track where the user is navigating in [_backingFiles]
|
||||
String _currentPath;
|
||||
late String _currentPath;
|
||||
var _picks = <File>[];
|
||||
|
||||
static final _log = Logger("widget.dir_picker_mixin.DirPickerMixin");
|
||||
|
|
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class Draggable<T> extends StatelessWidget {
|
||||
class Draggable<T extends Object> extends StatelessWidget {
|
||||
Draggable({
|
||||
Key key,
|
||||
@required this.data,
|
||||
@required this.child,
|
||||
Key? key,
|
||||
required this.data,
|
||||
required this.child,
|
||||
this.feedback,
|
||||
this.onDropBefore,
|
||||
this.onDropAfter,
|
||||
|
@ -79,7 +79,7 @@ class Draggable<T> extends StatelessWidget {
|
|||
},
|
||||
onAccept: (item) {
|
||||
_log.fine("[build] Dropping $item before $data");
|
||||
onDropBefore(item);
|
||||
onDropBefore!(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -92,7 +92,7 @@ class Draggable<T> extends StatelessWidget {
|
|||
},
|
||||
onAccept: (item) {
|
||||
_log.fine("[build] Dropping $item after $data");
|
||||
onDropAfter(item);
|
||||
onDropAfter!(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -105,26 +105,26 @@ class Draggable<T> extends StatelessWidget {
|
|||
|
||||
final T data;
|
||||
final Widget child;
|
||||
final Widget feedback;
|
||||
final Widget? feedback;
|
||||
|
||||
/// Called when some item dropped before this item
|
||||
final DragTargetAccept<T> onDropBefore;
|
||||
final DragTargetAccept<T>? onDropBefore;
|
||||
|
||||
/// Called when some item dropped after this item
|
||||
final DragTargetAccept<T> onDropAfter;
|
||||
final DragTargetAccept<T>? onDropAfter;
|
||||
|
||||
final VoidCallback onDragStarted;
|
||||
final VoidCallback? onDragStarted;
|
||||
|
||||
/// Called when either one of onDragEnd, onDragCompleted or
|
||||
/// onDraggableCanceled is called.
|
||||
///
|
||||
/// The callback might be called multiple times per each drag event
|
||||
final VoidCallback onDragEndedAny;
|
||||
final VoidCallback? onDragEndedAny;
|
||||
|
||||
/// Size of the feedback widget that appears under the pointer.
|
||||
///
|
||||
/// Right now a translucent version of [child] is being shown
|
||||
final Size feedbackSize;
|
||||
final Size? feedbackSize;
|
||||
|
||||
static final _log = Logger("widget.draggable.Draggable");
|
||||
}
|
||||
|
|
|
@ -9,21 +9,21 @@ abstract class DraggableItem {
|
|||
/// The widget to show under the pointer when a drag is under way.
|
||||
///
|
||||
/// Return null if you wish to just use the same widget as display
|
||||
Widget buildDragFeedbackWidget(BuildContext context) => null;
|
||||
Widget? buildDragFeedbackWidget(BuildContext context) => null;
|
||||
|
||||
bool get isDraggable => false;
|
||||
DragTargetAccept<DraggableItem> get onDropBefore => null;
|
||||
DragTargetAccept<DraggableItem> get onDropAfter => null;
|
||||
VoidCallback get onDragStarted => null;
|
||||
VoidCallback get onDragEndedAny => null;
|
||||
DragTargetAccept<DraggableItem>? get onDropBefore => null;
|
||||
DragTargetAccept<DraggableItem>? get onDropAfter => null;
|
||||
VoidCallback? get onDragStarted => null;
|
||||
VoidCallback? get onDragEndedAny => null;
|
||||
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
}
|
||||
|
||||
mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
|
||||
@protected
|
||||
Widget buildDraggableItemList({
|
||||
@required double maxCrossAxisExtent,
|
||||
ValueChanged<double> onMaxExtentChanged,
|
||||
required double maxCrossAxisExtent,
|
||||
ValueChanged<double?>? onMaxExtentChanged,
|
||||
}) {
|
||||
_maxCrossAxisExtent = maxCrossAxisExtent;
|
||||
return MeasurableItemList(
|
||||
|
@ -50,12 +50,10 @@ mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
|
|||
onDropAfter: item.onDropAfter,
|
||||
onDragStarted: item.onDragStarted,
|
||||
onDragEndedAny: item.onDragEndedAny,
|
||||
feedbackSize: _maxCrossAxisExtent != null
|
||||
? Size(_maxCrossAxisExtent * .65, _maxCrossAxisExtent * .65)
|
||||
: null,
|
||||
feedbackSize: Size(_maxCrossAxisExtent * .65, _maxCrossAxisExtent * .65),
|
||||
);
|
||||
}
|
||||
|
||||
var _items = <DraggableItem>[];
|
||||
double _maxCrossAxisExtent;
|
||||
late double _maxCrossAxisExtent;
|
||||
}
|
||||
|
|
|
@ -43,13 +43,18 @@ class DynamicAlbumViewerArguments {
|
|||
class DynamicAlbumViewer extends StatefulWidget {
|
||||
static const routeName = "/dynamic-album-viewer";
|
||||
|
||||
static Route buildRoute(DynamicAlbumViewerArguments args) =>
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DynamicAlbumViewer.fromArgs(args),
|
||||
);
|
||||
|
||||
DynamicAlbumViewer({
|
||||
Key key,
|
||||
@required this.account,
|
||||
@required this.album,
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.album,
|
||||
}) : super(key: key);
|
||||
|
||||
DynamicAlbumViewer.fromArgs(DynamicAlbumViewerArguments args, {Key key})
|
||||
DynamicAlbumViewer.fromArgs(DynamicAlbumViewerArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -96,19 +101,19 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
@override
|
||||
enterEditMode() {
|
||||
super.enterEditMode();
|
||||
_editAlbum = _album.copyWith();
|
||||
_editAlbum = _album!.copyWith();
|
||||
}
|
||||
|
||||
@override
|
||||
validateEditMode() => _editFormKey?.currentState?.validate() == true;
|
||||
validateEditMode() => _editFormKey.currentState?.validate() == true;
|
||||
|
||||
@override
|
||||
doneEditMode() {
|
||||
try {
|
||||
// persist the changes
|
||||
_editFormKey.currentState.save();
|
||||
final newAlbum = makeEdited(_editAlbum);
|
||||
if (newAlbum.copyWith(lastUpdated: OrNull(_album.lastUpdated)) !=
|
||||
_editFormKey.currentState!.save();
|
||||
final newAlbum = makeEdited(_editAlbum!);
|
||||
if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
|
||||
_album) {
|
||||
_log.info("[doneEditMode] Album modified: $newAlbum");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||
|
@ -142,14 +147,14 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
setState(() {
|
||||
_album = widget.album;
|
||||
_transformItems(items);
|
||||
final coverFile = initCover(widget.account, _backingFiles);
|
||||
_updateAlbumPostPopulate(coverFile, items);
|
||||
initCover(widget.account, _backingFiles);
|
||||
_updateAlbumPostPopulate(items);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updateAlbumPostPopulate(File coverFile, List<AlbumItem> items) {
|
||||
void _updateAlbumPostPopulate(List<AlbumItem> items) {
|
||||
List<File> timeDescSortedFiles;
|
||||
if (widget.album.sortProvider is AlbumTimeSortProvider) {
|
||||
if ((widget.album.sortProvider as AlbumTimeSortProvider).isAscending) {
|
||||
|
@ -168,7 +173,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
|
||||
bool shouldUpdate = false;
|
||||
final albumUpdatedCover = UpdateDynamicAlbumCover()
|
||||
.updateWithSortedFiles(_album, timeDescSortedFiles);
|
||||
.updateWithSortedFiles(_album!, timeDescSortedFiles);
|
||||
if (!identical(albumUpdatedCover, _album)) {
|
||||
_log.info("[_updateAlbumPostPopulate] Update album cover");
|
||||
shouldUpdate = true;
|
||||
|
@ -176,7 +181,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
_album = albumUpdatedCover;
|
||||
|
||||
final albumUpdatedTime = UpdateDynamicAlbumTime()
|
||||
.updateWithSortedFiles(_album, timeDescSortedFiles);
|
||||
.updateWithSortedFiles(_album!, timeDescSortedFiles);
|
||||
if (!identical(albumUpdatedTime, _album)) {
|
||||
_log.info(
|
||||
"[_updateAlbumPostPopulate] Update album time: ${albumUpdatedTime.provider.latestItemTime}");
|
||||
|
@ -185,7 +190,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
_album = albumUpdatedTime;
|
||||
|
||||
if (shouldUpdate) {
|
||||
UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(widget.account, _album);
|
||||
UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(widget.account, _album!);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,11 +246,11 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
return buildNormalAppBar(
|
||||
context,
|
||||
widget.account,
|
||||
_album,
|
||||
_album!,
|
||||
menuItemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _menuValueConvertBasic,
|
||||
child: Text(AppLocalizations.of(context).convertBasicAlbumMenuLabel),
|
||||
child: Text(AppLocalizations.of(context)!.convertBasicAlbumMenuLabel),
|
||||
),
|
||||
],
|
||||
onSelectedMenuItem: (option) {
|
||||
|
@ -267,7 +272,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
if (platform_k.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarSharePressed(context);
|
||||
},
|
||||
|
@ -277,7 +282,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _SelectionAppBarOption.delete,
|
||||
child: Text(AppLocalizations.of(context).deleteSelectedTooltip),
|
||||
child: Text(AppLocalizations.of(context)!.deleteSelectedTooltip),
|
||||
),
|
||||
],
|
||||
onSelected: (option) {
|
||||
|
@ -293,7 +298,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
return buildEditAppBar(context, widget.account, widget.album, actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.sort_by_alpha),
|
||||
tooltip: AppLocalizations.of(context).sortTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.sortTooltip,
|
||||
onPressed: _onEditAppBarSortPressed,
|
||||
),
|
||||
]);
|
||||
|
@ -317,9 +322,9 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.convertBasicAlbumConfirmationDialogTitle),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.convertBasicAlbumConfirmationDialogContent),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
|
@ -341,17 +346,17 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
return;
|
||||
}
|
||||
_log.info(
|
||||
"[_onAppBarConvertBasicPressed] Converting album '${_album.name}' to static");
|
||||
"[_onAppBarConvertBasicPressed] Converting album '${_album!.name}' to static");
|
||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||
UpdateAlbum(albumRepo)(
|
||||
widget.account,
|
||||
_album.copyWith(
|
||||
_album!.copyWith(
|
||||
provider: AlbumStaticProvider(items: _sortedItems),
|
||||
coverProvider: AlbumAutoCoverProvider(),
|
||||
),
|
||||
).then((value) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.convertBasicAlbumSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -386,7 +391,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
|
||||
void _onSelectionAppBarDeletePressed() async {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.deleteSelectedProcessingNotification(selectedListItems.length)),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
|
@ -417,12 +422,12 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
if (failures.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).deleteSelectedSuccessNotification),
|
||||
AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} else {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.deleteSelectedFailureNotification(failures.length)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -439,14 +444,14 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
}
|
||||
|
||||
void _onEditAppBarSortPressed() {
|
||||
final sortProvider = _editAlbum.sortProvider;
|
||||
final sortProvider = _editAlbum!.sortProvider;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FancyOptionPicker(
|
||||
title: AppLocalizations.of(context).sortOptionDialogTitle,
|
||||
title: AppLocalizations.of(context)!.sortOptionDialogTitle,
|
||||
items: [
|
||||
FancyOptionPickerItem(
|
||||
label: AppLocalizations.of(context).sortOptionTimeAscendingLabel,
|
||||
label: AppLocalizations.of(context)!.sortOptionTimeAscendingLabel,
|
||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||
sortProvider.isAscending,
|
||||
onSelect: () {
|
||||
|
@ -455,7 +460,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
},
|
||||
),
|
||||
FancyOptionPickerItem(
|
||||
label: AppLocalizations.of(context).sortOptionTimeDescendingLabel,
|
||||
label: AppLocalizations.of(context)!.sortOptionTimeDescendingLabel,
|
||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||
!sortProvider.isAscending,
|
||||
onSelect: () {
|
||||
|
@ -469,7 +474,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
}
|
||||
|
||||
void _onSortOldestPressed() {
|
||||
_editAlbum = _editAlbum.copyWith(
|
||||
_editAlbum = _editAlbum!.copyWith(
|
||||
sortProvider: AlbumTimeSortProvider(isAscending: true),
|
||||
);
|
||||
setState(() {
|
||||
|
@ -478,7 +483,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
}
|
||||
|
||||
void _onSortNewestPressed() {
|
||||
_editAlbum = _editAlbum.copyWith(
|
||||
_editAlbum = _editAlbum!.copyWith(
|
||||
sortProvider: AlbumTimeSortProvider(isAscending: false),
|
||||
);
|
||||
setState(() {
|
||||
|
@ -489,9 +494,9 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
void _transformItems(List<AlbumItem> items) {
|
||||
if (_editAlbum != null) {
|
||||
// edit mode
|
||||
_sortedItems = _editAlbum.sortProvider.sort(items);
|
||||
_sortedItems = _editAlbum!.sortProvider.sort(items);
|
||||
} else {
|
||||
_sortedItems = _album.sortProvider.sort(items);
|
||||
_sortedItems = _album!.sortProvider.sort(items);
|
||||
}
|
||||
_onSortedItemsUpdated();
|
||||
}
|
||||
|
@ -535,12 +540,12 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
.toList();
|
||||
}
|
||||
|
||||
Album _album;
|
||||
Album? _album;
|
||||
var _sortedItems = <AlbumItem>[];
|
||||
var _backingFiles = <File>[];
|
||||
|
||||
final _editFormKey = GlobalKey<FormState>();
|
||||
Album _editAlbum;
|
||||
Album? _editAlbum;
|
||||
|
||||
static final _log =
|
||||
Logger("widget.dynamic_album_viewer._DynamicAlbumViewerState");
|
||||
|
@ -549,8 +554,8 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
|||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
@required this.index,
|
||||
VoidCallback onTap,
|
||||
required this.index,
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
|
@ -571,14 +576,14 @@ abstract class _ListItem implements SelectableItem {
|
|||
|
||||
final int index;
|
||||
|
||||
final VoidCallback _onTap;
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
@required int index,
|
||||
@required this.file,
|
||||
VoidCallback onTap,
|
||||
required int index,
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(
|
||||
index: index,
|
||||
onTap: onTap,
|
||||
|
@ -589,11 +594,11 @@ abstract class _FileListItem extends _ListItem {
|
|||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
@required int index,
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
required int index,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(
|
||||
index: index,
|
||||
file: file,
|
||||
|
@ -615,11 +620,11 @@ class _ImageListItem extends _FileListItem {
|
|||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
@required int index,
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
required int index,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(
|
||||
index: index,
|
||||
file: file,
|
||||
|
|
|
@ -3,28 +3,28 @@ import 'package:flutter/widgets.dart';
|
|||
|
||||
class FancyOptionPickerItem {
|
||||
FancyOptionPickerItem({
|
||||
@required this.label,
|
||||
required this.label,
|
||||
this.isSelected = false,
|
||||
this.onSelect,
|
||||
});
|
||||
|
||||
String label;
|
||||
bool isSelected;
|
||||
VoidCallback onSelect;
|
||||
VoidCallback? onSelect;
|
||||
}
|
||||
|
||||
/// A fancy looking dialog to pick an option
|
||||
class FancyOptionPicker extends StatelessWidget {
|
||||
FancyOptionPicker({
|
||||
Key key,
|
||||
Key? key,
|
||||
this.title,
|
||||
@required this.items,
|
||||
required this.items,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: title != null ? Text(title) : null,
|
||||
title: title != null ? Text(title!) : null,
|
||||
children: items
|
||||
.map((e) => SimpleDialogOption(
|
||||
child: ListTile(
|
||||
|
@ -47,6 +47,6 @@ class FancyOptionPicker extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final String title;
|
||||
final String? title;
|
||||
final List<FancyOptionPickerItem> items;
|
||||
}
|
||||
|
|
|
@ -16,12 +16,16 @@ class HomeArguments {
|
|||
class Home extends StatefulWidget {
|
||||
static const routeName = "/home";
|
||||
|
||||
static Route buildRoute(HomeArguments args) => MaterialPageRoute(
|
||||
builder: (context) => Home.fromArgs(args),
|
||||
);
|
||||
|
||||
Home({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
Home.fromArgs(HomeArguments args, {Key key})
|
||||
Home.fromArgs(HomeArguments args, {Key? key})
|
||||
: this(
|
||||
account: args.account,
|
||||
);
|
||||
|
@ -33,12 +37,6 @@ class Home extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(initialPage: 0, keepPage: false);
|
||||
}
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return AppTheme(
|
||||
|
@ -54,11 +52,11 @@ class _HomeState extends State<Home> {
|
|||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.photo_outlined),
|
||||
label: AppLocalizations.of(context).photosTabLabel,
|
||||
label: AppLocalizations.of(context)!.photosTabLabel,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.photo_album_outlined),
|
||||
label: AppLocalizations.of(context).albumsTabLabel,
|
||||
label: AppLocalizations.of(context)!.albumsTabLabel,
|
||||
),
|
||||
],
|
||||
currentIndex: _nextPage,
|
||||
|
@ -108,6 +106,6 @@ class _HomeState extends State<Home> {
|
|||
});
|
||||
}
|
||||
|
||||
PageController _pageController;
|
||||
final _pageController = PageController(initialPage: 0, keepPage: false);
|
||||
int _nextPage = 0;
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
class HomeAlbums extends StatefulWidget {
|
||||
HomeAlbums({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -83,7 +83,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
_reqQuery();
|
||||
} else {
|
||||
// process the current state
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_onStateChange(context, _bloc.state);
|
||||
});
|
||||
|
@ -149,12 +149,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
});
|
||||
},
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.selectionAppBarTitle(_selectedItems.length)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: AppLocalizations.of(context).deleteSelectedTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.deleteSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarDeletePressed();
|
||||
},
|
||||
|
@ -171,13 +171,13 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
IconButton(
|
||||
onPressed: () => _onSearchPressed(context),
|
||||
icon: const Icon(Icons.search),
|
||||
tooltip: AppLocalizations.of(context).searchTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.searchTooltip,
|
||||
),
|
||||
],
|
||||
menuActions: [
|
||||
PopupMenuItem(
|
||||
value: _menuValueImport,
|
||||
child: Text(AppLocalizations.of(context).importFoldersTooltip),
|
||||
child: Text(AppLocalizations.of(context)!.importFoldersTooltip),
|
||||
),
|
||||
],
|
||||
onSelectedMenuActions: (option) {
|
||||
|
@ -225,7 +225,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
),
|
||||
),
|
||||
),
|
||||
title: AppLocalizations.of(context).albumArchiveLabel,
|
||||
title: AppLocalizations.of(context)!.albumArchiveLabel,
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(ArchiveViewer.routeName,
|
||||
arguments: ArchiveViewerArguments(widget.account));
|
||||
|
@ -247,7 +247,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
),
|
||||
),
|
||||
),
|
||||
title: AppLocalizations.of(context).createAlbumTooltip,
|
||||
title: AppLocalizations.of(context)!.createAlbumTooltip,
|
||||
onTap: () => _onNewAlbumItemTap(context),
|
||||
);
|
||||
}
|
||||
|
@ -324,7 +324,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
"[_onNewAlbumItemTap] Failed while showDialog", e, stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).createAlbumFailureNotification),
|
||||
Text(AppLocalizations.of(context)!.createAlbumFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
});
|
||||
|
@ -337,11 +337,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
|
||||
Future<void> _onSelectionAppBarDeletePressed() async {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.deleteSelectedProcessingNotification(_selectedItems.length)),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
final selectedFiles = _selectedItems.map((e) => e.album.albumFile).toList();
|
||||
final selectedFiles =
|
||||
_selectedItems.map((e) => e.album.albumFile!).toList();
|
||||
setState(() {
|
||||
_selectedItems.clear();
|
||||
});
|
||||
|
@ -363,12 +364,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
if (failures.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).deleteSelectedSuccessNotification),
|
||||
AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} else {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.deleteSelectedFailureNotification(failures.length)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -411,12 +412,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
.map((from) {
|
||||
try {
|
||||
return _items.whereType<_GridItem>().firstWhere(
|
||||
(to) => from.album.albumFile.path == to.album.albumFile.path);
|
||||
(to) => from.album.albumFile!.path == to.album.albumFile!.path);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.where((element) => element != null)
|
||||
.whereType<_GridItem>()
|
||||
.toList();
|
||||
_selectedItems
|
||||
..clear()
|
||||
|
@ -439,7 +440,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
|||
|
||||
bool get _isSelectionMode => _selectedItems.isNotEmpty;
|
||||
|
||||
ListAlbumBloc _bloc;
|
||||
late ListAlbumBloc _bloc;
|
||||
|
||||
final _items = <_GridItem>[];
|
||||
final _selectedItems = <_GridItem>[];
|
||||
|
|
|
@ -13,8 +13,8 @@ import 'package:nc_photos/widget/settings.dart';
|
|||
/// AppBar for home screens
|
||||
class HomeSliverAppBar extends StatelessWidget {
|
||||
HomeSliverAppBar({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
this.actions,
|
||||
this.menuActions,
|
||||
this.onSelectedMenuActions,
|
||||
|
@ -90,7 +90,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
|||
inactiveThumbImage:
|
||||
const AssetImage("assets/ic_dark_mode_switch_24dp.png"),
|
||||
),
|
||||
PopupMenuButton(
|
||||
PopupMenuButton<int>(
|
||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||
itemBuilder: (context) =>
|
||||
(menuActions ?? []) +
|
||||
|
@ -98,7 +98,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
|||
PopupMenuItem(
|
||||
value: _menuValueAbout,
|
||||
child:
|
||||
Text(AppLocalizations.of(context).settingsMenuLabel),
|
||||
Text(AppLocalizations.of(context)!.settingsMenuLabel),
|
||||
),
|
||||
],
|
||||
onSelected: (option) {
|
||||
|
@ -125,12 +125,12 @@ class HomeSliverAppBar extends StatelessWidget {
|
|||
final Account account;
|
||||
|
||||
/// Screen specific action buttons
|
||||
final List<Widget> actions;
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// Screen specific actions under the overflow menu. The value of each item
|
||||
/// much >= 0
|
||||
final List<PopupMenuEntry<int>> menuActions;
|
||||
final void Function(int) onSelectedMenuActions;
|
||||
final List<PopupMenuEntry<int>>? menuActions;
|
||||
final void Function(int)? onSelectedMenuActions;
|
||||
|
||||
static const _menuValueAbout = -1;
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ import 'package:nc_photos/widget/viewer.dart';
|
|||
|
||||
class HomePhotos extends StatefulWidget {
|
||||
HomePhotos({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -59,7 +59,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevel(0);
|
||||
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevelOr(0);
|
||||
_initBloc();
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
_reqQuery();
|
||||
} else {
|
||||
// process the current state
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
_onStateChange(context, _bloc.state);
|
||||
});
|
||||
|
@ -162,35 +162,36 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
});
|
||||
},
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.selectionAppBarTitle(selectedListItems.length)),
|
||||
actions: [
|
||||
if (platform_k.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarSharePressed(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.playlist_add),
|
||||
tooltip: AppLocalizations.of(context).addSelectedToAlbumTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.addSelectedToAlbumTooltip,
|
||||
onPressed: () {
|
||||
_onSelectionAppBarAddToAlbumPressed(context);
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
PopupMenuButton<_SelectionAppBarMenuOption>(
|
||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _SelectionAppBarMenuOption.archive,
|
||||
child:
|
||||
Text(AppLocalizations.of(context).archiveSelectedMenuLabel),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.archiveSelectedMenuLabel),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _SelectionAppBarMenuOption.delete,
|
||||
child: Text(AppLocalizations.of(context).deleteSelectedTooltip),
|
||||
child:
|
||||
Text(AppLocalizations.of(context)!.deleteSelectedTooltip),
|
||||
),
|
||||
],
|
||||
onSelected: (option) {
|
||||
|
@ -212,7 +213,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.photo_size_select_large),
|
||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.zoomTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuZoom(
|
||||
initialValue: _thumbZoomLevel,
|
||||
|
@ -231,7 +232,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
menuActions: [
|
||||
PopupMenuItem(
|
||||
value: _menuValueRefresh,
|
||||
child: Text(AppLocalizations.of(context).refreshMenuLabel),
|
||||
child: Text(AppLocalizations.of(context)!.refreshMenuLabel),
|
||||
),
|
||||
],
|
||||
onSelectedMenuActions: (option) {
|
||||
|
@ -251,7 +252,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
} else if (state is ScanDirBlocSuccess || state is ScanDirBlocLoading) {
|
||||
_transformItems(state.files);
|
||||
if (state is ScanDirBlocSuccess) {
|
||||
if (Pref.inst().isEnableExif() && !_hasFiredMetadataTask.value) {
|
||||
if (Pref.inst().isEnableExifOr() && !_hasFiredMetadataTask.value) {
|
||||
KiwiContainer()
|
||||
.resolve<MetadataTaskManager>()
|
||||
.addTask(MetadataTask(widget.account));
|
||||
|
@ -306,14 +307,14 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
clearSelectedItems();
|
||||
});
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.addSelectedToAlbumSuccessNotification(value.name)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}).catchError((_) {});
|
||||
} else {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.addSelectedToAlbumFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -325,7 +326,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
"${AppLocalizations.of(context).addSelectedToAlbumFailureNotification}: "
|
||||
"${AppLocalizations.of(context)!.addSelectedToAlbumFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -355,7 +356,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
"[_addSelectedToAlbum] Failed while updating album", e, stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
"${AppLocalizations.of(context).addSelectedToAlbumFailureNotification}: "
|
||||
"${AppLocalizations.of(context)!.addSelectedToAlbumFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -365,7 +366,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
Future<void> _onSelectionAppBarDeletePressed(BuildContext context) async {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.deleteSelectedProcessingNotification(selectedListItems.length)),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
|
@ -394,12 +395,12 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
if (failures.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).deleteSelectedSuccessNotification),
|
||||
AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} else {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.deleteSelectedFailureNotification(failures.length)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -425,7 +426,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
Future<void> _onSelectionAppBarArchivePressed(BuildContext context) async {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.archiveSelectedProcessingNotification(selectedListItems.length)),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
|
@ -454,12 +455,12 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
if (failures.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).archiveSelectedSuccessNotification),
|
||||
AppLocalizations.of(context)!.archiveSelectedSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} else {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.archiveSelectedFailureNotification(failures.length)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -478,16 +479,16 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
file_util.isSupportedFormat(element) && element.isArchived != true)
|
||||
.sorted(compareFileDateTimeDescending);
|
||||
|
||||
DateTime currentDate;
|
||||
DateTime? currentDate;
|
||||
final isMonthOnly = _thumbZoomLevel < 0;
|
||||
itemStreamListItems = () sync* {
|
||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||
final f = _backingFiles[i];
|
||||
|
||||
final newDate = f.bestDateTime?.toLocal();
|
||||
if (newDate?.year != currentDate?.year ||
|
||||
newDate?.month != currentDate?.month ||
|
||||
(!isMonthOnly && newDate?.day != currentDate?.day)) {
|
||||
final newDate = f.bestDateTime.toLocal();
|
||||
if (newDate.year != currentDate?.year ||
|
||||
newDate.month != currentDate?.month ||
|
||||
(!isMonthOnly && newDate.day != currentDate?.day)) {
|
||||
yield _DateListItem(date: newDate, isMonthOnly: isMonthOnly);
|
||||
currentDate = newDate;
|
||||
}
|
||||
|
@ -546,13 +547,13 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
|
||||
/// Return the estimated scroll extent of the custom scroll view, or null
|
||||
double _getScrollViewExtent(BoxConstraints constraints) {
|
||||
double? _getScrollViewExtent(BoxConstraints constraints) {
|
||||
if (_itemListMaxExtent != null &&
|
||||
constraints.hasBoundedHeight &&
|
||||
_appBarExtent != null) {
|
||||
// scroll extent = list height - widget viewport height + sliver app bar height + list padding
|
||||
final scrollExtent =
|
||||
_itemListMaxExtent - constraints.maxHeight + _appBarExtent + 16;
|
||||
_itemListMaxExtent! - constraints.maxHeight + _appBarExtent! + 16;
|
||||
_log.info(
|
||||
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + 16 = $scrollExtent");
|
||||
return scrollExtent;
|
||||
|
@ -595,7 +596,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
}
|
||||
|
||||
ScanDirBloc _bloc;
|
||||
late ScanDirBloc _bloc;
|
||||
|
||||
var _backingFiles = <File>[];
|
||||
|
||||
|
@ -603,8 +604,8 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
double _appBarExtent;
|
||||
double _itemListMaxExtent;
|
||||
double? _appBarExtent;
|
||||
double? _itemListMaxExtent;
|
||||
|
||||
static final _log = Logger("widget.home_photos._HomePhotosState");
|
||||
static const _menuValueRefresh = 0;
|
||||
|
@ -612,7 +613,7 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
abstract class _ListItem implements SelectableItem {
|
||||
_ListItem({
|
||||
VoidCallback onTap,
|
||||
VoidCallback? onTap,
|
||||
}) : _onTap = onTap;
|
||||
|
||||
@override
|
||||
|
@ -624,12 +625,12 @@ abstract class _ListItem implements SelectableItem {
|
|||
@override
|
||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
|
||||
final VoidCallback _onTap;
|
||||
final VoidCallback? _onTap;
|
||||
}
|
||||
|
||||
class _DateListItem extends _ListItem {
|
||||
_DateListItem({
|
||||
@required this.date,
|
||||
required this.date,
|
||||
this.isMonthOnly = false,
|
||||
});
|
||||
|
||||
|
@ -647,7 +648,7 @@ class _DateListItem extends _ListItem {
|
|||
isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY;
|
||||
subtitle =
|
||||
DateFormat(pattern, Localizations.localeOf(context).languageCode)
|
||||
.format(date.toLocal());
|
||||
.format(date!.toLocal());
|
||||
}
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
|
@ -655,7 +656,7 @@ class _DateListItem extends _ListItem {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
style: Theme.of(context).textTheme.caption!.copyWith(
|
||||
color: AppTheme.getPrimaryTextColor(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
|
@ -664,14 +665,14 @@ class _DateListItem extends _ListItem {
|
|||
);
|
||||
}
|
||||
|
||||
final DateTime date;
|
||||
final DateTime? date;
|
||||
final bool isMonthOnly;
|
||||
}
|
||||
|
||||
abstract class _FileListItem extends _ListItem {
|
||||
_FileListItem({
|
||||
@required this.file,
|
||||
VoidCallback onTap,
|
||||
required this.file,
|
||||
VoidCallback? onTap,
|
||||
}) : super(onTap: onTap);
|
||||
|
||||
@override
|
||||
|
@ -687,10 +688,10 @@ abstract class _FileListItem extends _ListItem {
|
|||
|
||||
class _ImageListItem extends _FileListItem {
|
||||
_ImageListItem({
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
|
@ -708,10 +709,10 @@ class _ImageListItem extends _FileListItem {
|
|||
|
||||
class _VideoListItem extends _FileListItem {
|
||||
_VideoListItem({
|
||||
@required File file,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
VoidCallback onTap,
|
||||
required File file,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
VoidCallback? onTap,
|
||||
}) : super(file: file, onTap: onTap);
|
||||
|
||||
@override
|
||||
|
|
|
@ -11,9 +11,9 @@ import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
|
|||
|
||||
class ImageViewer extends StatefulWidget {
|
||||
ImageViewer({
|
||||
@required this.account,
|
||||
@required this.file,
|
||||
this.canZoom,
|
||||
required this.account,
|
||||
required this.file,
|
||||
required this.canZoom,
|
||||
this.onLoaded,
|
||||
this.onHeightChanged,
|
||||
this.onZoomStarted,
|
||||
|
@ -35,10 +35,10 @@ class ImageViewer extends StatefulWidget {
|
|||
final Account account;
|
||||
final File file;
|
||||
final bool canZoom;
|
||||
final VoidCallback onLoaded;
|
||||
final void Function(double height) onHeightChanged;
|
||||
final VoidCallback onZoomStarted;
|
||||
final VoidCallback onZoomEnded;
|
||||
final VoidCallback? onLoaded;
|
||||
final ValueChanged<double>? onHeightChanged;
|
||||
final VoidCallback? onZoomStarted;
|
||||
final VoidCallback? onZoomEnded;
|
||||
}
|
||||
|
||||
class _ImageViewerState extends State<ImageViewer>
|
||||
|
@ -58,9 +58,9 @@ class _ImageViewerState extends State<ImageViewer>
|
|||
alignment: Alignment.center,
|
||||
child: NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
if (_key.currentContext != null) {
|
||||
widget.onHeightChanged?.call(_key.currentContext.size.height);
|
||||
widget.onHeightChanged?.call(_key.currentContext!.size!.height);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
|
@ -78,7 +78,7 @@ class _ImageViewerState extends State<ImageViewer>
|
|||
filterQuality: FilterQuality.high,
|
||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||
imageBuilder: (context, child, imageProvider) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
_onItemLoaded();
|
||||
});
|
||||
SizeChangedLayoutNotification().dispatch(context);
|
||||
|
|
|
@ -10,11 +10,11 @@ abstract class MeasurableItemListState {
|
|||
|
||||
class MeasurableItemList extends StatefulWidget {
|
||||
MeasurableItemList({
|
||||
Key key,
|
||||
@required this.maxCrossAxisExtent,
|
||||
@required this.itemCount,
|
||||
@required this.itemBuilder,
|
||||
@required this.staggeredTileBuilder,
|
||||
Key? key,
|
||||
required this.maxCrossAxisExtent,
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
required this.staggeredTileBuilder,
|
||||
this.onMaxExtentChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -25,7 +25,7 @@ class MeasurableItemList extends StatefulWidget {
|
|||
final int itemCount;
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
final IndexedStaggeredTileBuilder staggeredTileBuilder;
|
||||
final ValueChanged<double> onMaxExtentChanged;
|
||||
final ValueChanged<double?>? onMaxExtentChanged;
|
||||
}
|
||||
|
||||
class _MeasurableItemListState extends State<MeasurableItemList>
|
||||
|
@ -35,21 +35,21 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
|||
initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
_prevOrientation = MediaQuery.of(context).orientation;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance!.addObserver(this);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
didChangeMetrics() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
if (orientation != _prevOrientation) {
|
||||
_log.info(
|
||||
|
@ -70,7 +70,8 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
|||
}
|
||||
if (constraints.crossAxisExtent != _prevListWidth) {
|
||||
_log.info("[build] updateListHeight: list viewport width changed");
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight());
|
||||
WidgetsBinding.instance!
|
||||
.addPostFrameCallback((_) => updateListHeight());
|
||||
_prevListWidth = constraints.crossAxisExtent;
|
||||
}
|
||||
|
||||
|
@ -78,7 +79,8 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
|||
final cellSize = widget.maxCrossAxisExtent;
|
||||
if (cellSize != _prevCellSize) {
|
||||
_log.info("[build] updateListHeight: cell size changed");
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight());
|
||||
WidgetsBinding.instance!
|
||||
.addPostFrameCallback((_) => updateListHeight());
|
||||
_prevCellSize = cellSize;
|
||||
}
|
||||
_gridKey = _GridKey("$_uniqueToken $cellSize");
|
||||
|
@ -94,9 +96,9 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
|||
|
||||
@override
|
||||
updateListHeight() {
|
||||
double newMaxExtent;
|
||||
double? newMaxExtent;
|
||||
try {
|
||||
final renderObj = _gridKey.currentContext.findRenderObject()
|
||||
final renderObj = _gridKey.currentContext!.findRenderObject()
|
||||
as RenderMeasurableSliverStaggeredGrid;
|
||||
final maxExtent = renderObj.calculateExtent();
|
||||
_log.info("[updateListHeight] Max extent: $maxExtent");
|
||||
|
@ -118,14 +120,14 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
|||
}
|
||||
}
|
||||
|
||||
double _prevListWidth;
|
||||
double _prevCellSize;
|
||||
double _maxExtent;
|
||||
Orientation _prevOrientation;
|
||||
double? _prevListWidth;
|
||||
double? _prevCellSize;
|
||||
double? _maxExtent;
|
||||
Orientation? _prevOrientation;
|
||||
|
||||
// this unique token is there to keep the global key unique
|
||||
final _uniqueToken = Uuid().v4();
|
||||
GlobalObjectKey _gridKey;
|
||||
late GlobalObjectKey _gridKey;
|
||||
|
||||
static final _log =
|
||||
Logger("widget.measurable_item_list._MeasurableItemListState");
|
||||
|
|
|
@ -8,9 +8,9 @@ class MeasureSize extends SingleChildRenderObjectWidget {
|
|||
final OnWidgetSizeChanged onChange;
|
||||
|
||||
const MeasureSize({
|
||||
Key key,
|
||||
@required this.onChange,
|
||||
@required Widget child,
|
||||
Key? key,
|
||||
required this.onChange,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
|
@ -20,7 +20,7 @@ class MeasureSize extends SingleChildRenderObjectWidget {
|
|||
}
|
||||
|
||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||
Size oldSize;
|
||||
Size? oldSize;
|
||||
final OnWidgetSizeChanged onChange;
|
||||
|
||||
_MeasureSizeRenderObject(this.onChange);
|
||||
|
@ -29,11 +29,11 @@ class _MeasureSizeRenderObject extends RenderProxyBox {
|
|||
void performLayout() {
|
||||
super.performLayout();
|
||||
|
||||
Size newSize = child.size;
|
||||
if (oldSize == newSize) return;
|
||||
var newSize = child?.size;
|
||||
if (newSize == null || oldSize == newSize) return;
|
||||
|
||||
oldSize = newSize;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
onChange(newSize);
|
||||
});
|
||||
}
|
||||
|
@ -41,9 +41,9 @@ class _MeasureSizeRenderObject extends RenderProxyBox {
|
|||
|
||||
class SliverMeasureExtent extends SingleChildRenderObjectWidget {
|
||||
const SliverMeasureExtent({
|
||||
Key key,
|
||||
@required this.onChange,
|
||||
@required Widget child,
|
||||
Key? key,
|
||||
required this.onChange,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
|
@ -61,16 +61,16 @@ class _SliverMeasureExtentRenderObject extends RenderProxySliver {
|
|||
void performLayout() {
|
||||
super.performLayout();
|
||||
|
||||
double newExent = child.geometry.scrollExtent;
|
||||
if (_oldExtent == newExent) {
|
||||
var newExent = child?.geometry?.scrollExtent;
|
||||
if (newExent == null || _oldExtent == newExent) {
|
||||
return;
|
||||
}
|
||||
|
||||
_oldExtent = newExent;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onChange(newExent));
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) => onChange(newExent));
|
||||
}
|
||||
|
||||
final void Function(double) onChange;
|
||||
|
||||
double _oldExtent;
|
||||
double? _oldExtent;
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|||
// ignore: must_be_immutable
|
||||
class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid {
|
||||
MeasurableSliverStaggeredGrid.extentBuilder({
|
||||
Key key,
|
||||
@required double maxCrossAxisExtent,
|
||||
@required IndexedStaggeredTileBuilder staggeredTileBuilder,
|
||||
@required IndexedWidgetBuilder itemBuilder,
|
||||
@required int itemCount,
|
||||
Key? key,
|
||||
required double maxCrossAxisExtent,
|
||||
required IndexedStaggeredTileBuilder staggeredTileBuilder,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
required int itemCount,
|
||||
double mainAxisSpacing = 0,
|
||||
double crossAxisSpacing = 0,
|
||||
}) : super(
|
||||
|
@ -32,19 +32,19 @@ class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid {
|
|||
final element = context as SliverVariableSizeBoxAdaptorElement;
|
||||
_renderObject = RenderMeasurableSliverStaggeredGrid(
|
||||
childManager: element, gridDelegate: gridDelegate);
|
||||
return _renderObject;
|
||||
return _renderObject!;
|
||||
}
|
||||
|
||||
RenderMeasurableSliverStaggeredGrid get renderObject => _renderObject;
|
||||
RenderMeasurableSliverStaggeredGrid? get renderObject => _renderObject;
|
||||
|
||||
RenderMeasurableSliverStaggeredGrid _renderObject;
|
||||
RenderMeasurableSliverStaggeredGrid? _renderObject;
|
||||
}
|
||||
|
||||
class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid
|
||||
with WidgetsBindingObserver {
|
||||
RenderMeasurableSliverStaggeredGrid({
|
||||
@required RenderSliverVariableSizeBoxChildManager childManager,
|
||||
@required SliverStaggeredGridDelegate gridDelegate,
|
||||
required RenderSliverVariableSizeBoxChildManager childManager,
|
||||
required SliverStaggeredGridDelegate gridDelegate,
|
||||
}) : super(childManager: childManager, gridDelegate: gridDelegate);
|
||||
|
||||
/// Calculate the height of this staggered grid view
|
||||
|
@ -69,13 +69,13 @@ class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid
|
|||
}
|
||||
|
||||
final bool hasTrailingScrollOffset = geometry.hasTrailingScrollOffset;
|
||||
RenderBox child;
|
||||
RenderBox? child;
|
||||
if (!hasTrailingScrollOffset) {
|
||||
// Layout the child to compute its tailingScrollOffset.
|
||||
final constraints =
|
||||
BoxConstraints.tightFor(width: geometry.crossAxisExtent);
|
||||
child = addAndLayoutChild(index, constraints, parentUsesSize: true);
|
||||
geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child));
|
||||
geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child!));
|
||||
}
|
||||
|
||||
if (child != null) {
|
||||
|
|
|
@ -2,6 +2,7 @@ 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/account.dart';
|
||||
import 'package:nc_photos/event/event.dart';
|
||||
import 'package:nc_photos/language_util.dart' as language_util;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
|
@ -44,10 +45,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
@override
|
||||
build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
|
||||
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
|
||||
theme: _getLightTheme(),
|
||||
darkTheme: _getDarkTheme(),
|
||||
themeMode: Pref.inst().isDarkTheme() ? ThemeMode.dark : ThemeMode.light,
|
||||
themeMode:
|
||||
Pref.inst().isDarkThemeOr(false) ? ThemeMode.dark : ThemeMode.light,
|
||||
initialRoute: Splash.routeName,
|
||||
onGenerateRoute: _onGenerateRoute,
|
||||
navigatorObservers: <NavigatorObserver>[MyApp.routeObserver],
|
||||
|
@ -87,9 +89,9 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
Splash.routeName: (context) => Splash(),
|
||||
};
|
||||
|
||||
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
||||
_log.info("[_onGenerateRoute] Route: ${settings.name}");
|
||||
Route<dynamic> route;
|
||||
Route<dynamic>? route;
|
||||
route ??= _handleBasicRoute(settings);
|
||||
route ??= _handleViewerRoute(settings);
|
||||
route ??= _handleConnectRoute(settings);
|
||||
|
@ -112,7 +114,7 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Route<dynamic> _handleBasicRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleBasicRoute(RouteSettings settings) {
|
||||
for (final e in _getRouter().entries) {
|
||||
if (e.key == settings.name) {
|
||||
return MaterialPageRoute(
|
||||
|
@ -123,13 +125,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleViewerRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleViewerRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == Viewer.routeName && settings.arguments != null) {
|
||||
final ViewerArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => Viewer.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as ViewerArguments;
|
||||
return Viewer.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleViewerRoute] Failed while handling route", e);
|
||||
|
@ -137,13 +137,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleConnectRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleConnectRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == Connect.routeName && settings.arguments != null) {
|
||||
final ConnectArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => Connect.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as ConnectArguments;
|
||||
return Connect.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleConnectRoute] Failed while handling route", e);
|
||||
|
@ -151,13 +149,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleHomeRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleHomeRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == Home.routeName && settings.arguments != null) {
|
||||
final HomeArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => Home.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as HomeArguments;
|
||||
return Home.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleHomeRoute] Failed while handling route", e);
|
||||
|
@ -165,13 +161,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleRootPickerRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleRootPickerRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == RootPicker.routeName && settings.arguments != null) {
|
||||
final RootPickerArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => RootPicker.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as RootPickerArguments;
|
||||
return RootPicker.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleRootPickerRoute] Failed while handling route", e);
|
||||
|
@ -179,14 +173,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleAlbumViewerRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleAlbumViewerRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == AlbumViewer.routeName &&
|
||||
settings.arguments != null) {
|
||||
final AlbumViewerArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => AlbumViewer.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as AlbumViewerArguments;
|
||||
return AlbumViewer.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleAlbumViewerRoute] Failed while handling route", e);
|
||||
|
@ -194,13 +186,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleSettingsRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleSettingsRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == Settings.routeName && settings.arguments != null) {
|
||||
final SettingsArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => Settings.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as SettingsArguments;
|
||||
return Settings.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleSettingsRoute] Failed while handling route", e);
|
||||
|
@ -208,14 +198,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleArchiveViewerRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleArchiveViewerRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == ArchiveViewer.routeName &&
|
||||
settings.arguments != null) {
|
||||
final ArchiveViewerArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => ArchiveViewer.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as ArchiveViewerArguments;
|
||||
return ArchiveViewer.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleArchiveViewerRoute] Failed while handling route", e);
|
||||
|
@ -223,14 +211,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleDynamicAlbumViewerRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleDynamicAlbumViewerRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == DynamicAlbumViewer.routeName &&
|
||||
settings.arguments != null) {
|
||||
final DynamicAlbumViewerArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => DynamicAlbumViewer.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as DynamicAlbumViewerArguments;
|
||||
return DynamicAlbumViewer.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe(
|
||||
|
@ -239,14 +225,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleAlbumDirPickerRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleAlbumDirPickerRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == AlbumDirPicker.routeName &&
|
||||
settings.arguments != null) {
|
||||
final AlbumDirPickerArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => AlbumDirPicker.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as AlbumDirPickerArguments;
|
||||
return AlbumDirPicker.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe(
|
||||
|
@ -255,14 +239,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic> _handleAlbumImporterRoute(RouteSettings settings) {
|
||||
Route<dynamic>? _handleAlbumImporterRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == AlbumImporter.routeName &&
|
||||
settings.arguments != null) {
|
||||
final AlbumImporterArguments args = settings.arguments;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => AlbumImporter.fromArgs(args),
|
||||
);
|
||||
final args = settings.arguments as AlbumImporterArguments;
|
||||
return AlbumImporter.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleAlbumImporterRoute] Failed while handling route", e);
|
||||
|
@ -272,8 +254,8 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
|||
|
||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
AppEventListener<ThemeChangedEvent> _themeChangedListener;
|
||||
AppEventListener<LanguageChangedEvent> _langChangedListener;
|
||||
late AppEventListener<ThemeChangedEvent> _themeChangedListener;
|
||||
late AppEventListener<LanguageChangedEvent> _langChangedListener;
|
||||
|
||||
static final _log = Logger("widget.my_app.MyAppState");
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album.dart';
|
|||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||
import 'package:nc_photos/entity/album/provider.dart';
|
||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/use_case/create_album.dart';
|
||||
import 'package:nc_photos/widget/album_dir_picker.dart';
|
||||
|
||||
|
@ -16,8 +17,8 @@ import 'package:nc_photos/widget/album_dir_picker.dart';
|
|||
/// cancelled
|
||||
class NewAlbumDialog extends StatefulWidget {
|
||||
NewAlbumDialog({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
this.isAllowDynamic = true,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -39,7 +40,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
return Visibility(
|
||||
visible: _isVisible,
|
||||
child: AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).createAlbumTooltip),
|
||||
title: Text(AppLocalizations.of(context)!.createAlbumTooltip),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: Container(
|
||||
|
@ -50,17 +51,17 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
children: [
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context).nameInputHint,
|
||||
hintText: AppLocalizations.of(context)!.nameInputHint,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return AppLocalizations.of(context)
|
||||
if (value!.isEmpty) {
|
||||
return AppLocalizations.of(context)!
|
||||
.albumNameInputInvalidEmpty;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.name = value;
|
||||
_formValue.name = value!;
|
||||
},
|
||||
),
|
||||
if (widget.isAllowDynamic) ...[
|
||||
|
@ -75,7 +76,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
.toList(),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
_provider = newValue;
|
||||
_provider = newValue!;
|
||||
});
|
||||
},
|
||||
onSaved: (value) {
|
||||
|
@ -104,8 +105,8 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
}
|
||||
|
||||
void _onOkPressed(BuildContext context) {
|
||||
if (_formKey.currentState.validate()) {
|
||||
_formKey.currentState.save();
|
||||
if (_formKey.currentState?.validate() == true) {
|
||||
_formKey.currentState!.save();
|
||||
if (_formValue.provider == _Provider.static ||
|
||||
_formValue.provider == null) {
|
||||
_onConfirmStaticAlbum();
|
||||
|
@ -136,7 +137,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
_isVisible = false;
|
||||
});
|
||||
Navigator.of(context)
|
||||
.pushNamed(AlbumDirPicker.routeName,
|
||||
.pushNamed<List<File>>(AlbumDirPicker.routeName,
|
||||
arguments: AlbumDirPickerArguments(widget.account))
|
||||
.then((value) {
|
||||
if (value == null) {
|
||||
|
@ -174,8 +175,8 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
|||
}
|
||||
|
||||
class _FormValue {
|
||||
String name;
|
||||
_Provider provider;
|
||||
late String name;
|
||||
_Provider? provider;
|
||||
}
|
||||
|
||||
enum _Provider {
|
||||
|
@ -187,10 +188,10 @@ extension on _Provider {
|
|||
String toValueString(BuildContext context) {
|
||||
switch (this) {
|
||||
case _Provider.static:
|
||||
return AppLocalizations.of(context).createAlbumDialogBasicLabel;
|
||||
return AppLocalizations.of(context)!.createAlbumDialogBasicLabel;
|
||||
|
||||
case _Provider.dir:
|
||||
return AppLocalizations.of(context).createAlbumDialogFolderBasedLabel;
|
||||
return AppLocalizations.of(context)!.createAlbumDialogFolderBasedLabel;
|
||||
|
||||
default:
|
||||
throw StateError("Unknown value: $this");
|
||||
|
@ -200,10 +201,10 @@ extension on _Provider {
|
|||
String toDescription(BuildContext context) {
|
||||
switch (this) {
|
||||
case _Provider.static:
|
||||
return AppLocalizations.of(context).createAlbumDialogBasicDescription;
|
||||
return AppLocalizations.of(context)!.createAlbumDialogBasicDescription;
|
||||
|
||||
case _Provider.dir:
|
||||
return AppLocalizations.of(context)
|
||||
return AppLocalizations.of(context)!
|
||||
.createAlbumDialogFolderBasedDescription;
|
||||
|
||||
default:
|
||||
|
|
|
@ -5,7 +5,7 @@ mixin PageVisibilityMixin<T extends StatefulWidget> on State<T>, RouteAware {
|
|||
@override
|
||||
didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
MyApp.routeObserver.subscribe(this, ModalRoute.of(context));
|
||||
MyApp.routeObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -6,8 +6,8 @@ import 'package:nc_photos/num_extension.dart';
|
|||
|
||||
class PhotoDateTimeEditDialog extends StatefulWidget {
|
||||
PhotoDateTimeEditDialog({
|
||||
Key key,
|
||||
@required this.initialDateTime,
|
||||
Key? key,
|
||||
required this.initialDateTime,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -20,7 +20,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
@override
|
||||
build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).updateDateTimeDialogTitle),
|
||||
title: Text(AppLocalizations.of(context)!.updateDateTimeDialogTitle),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: Container(
|
||||
|
@ -29,7 +29,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).dateSubtitle,
|
||||
AppLocalizations.of(context)!.dateSubtitle,
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
),
|
||||
Row(
|
||||
|
@ -38,20 +38,20 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
AppLocalizations.of(context).dateYearInputHint,
|
||||
AppLocalizations.of(context)!.dateYearInputHint,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
try {
|
||||
int.parse(value);
|
||||
int.parse(value!);
|
||||
return null;
|
||||
} catch (_) {
|
||||
return AppLocalizations.of(context)
|
||||
return AppLocalizations.of(context)!
|
||||
.dateTimeInputInvalid;
|
||||
}
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.year = int.parse(value);
|
||||
_formValue.year = int.parse(value!);
|
||||
},
|
||||
initialValue: "${widget.initialDateTime.year}",
|
||||
),
|
||||
|
@ -62,18 +62,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
AppLocalizations.of(context).dateMonthInputHint,
|
||||
AppLocalizations.of(context)!.dateMonthInputHint,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (int.tryParse(value)?.inRange(1, 12) == true) {
|
||||
if (int.tryParse(value!)?.inRange(1, 12) == true) {
|
||||
return null;
|
||||
}
|
||||
return AppLocalizations.of(context)
|
||||
return AppLocalizations.of(context)!
|
||||
.dateTimeInputInvalid;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.month = int.parse(value);
|
||||
_formValue.month = int.parse(value!);
|
||||
},
|
||||
initialValue: widget.initialDateTime.month
|
||||
.toString()
|
||||
|
@ -85,18 +85,19 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
Flexible(
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context).dateDayInputHint,
|
||||
hintText:
|
||||
AppLocalizations.of(context)!.dateDayInputHint,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (int.tryParse(value)?.inRange(1, 31) == true) {
|
||||
if (int.tryParse(value!)?.inRange(1, 31) == true) {
|
||||
return null;
|
||||
}
|
||||
return AppLocalizations.of(context)
|
||||
return AppLocalizations.of(context)!
|
||||
.dateTimeInputInvalid;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.day = int.parse(value);
|
||||
_formValue.day = int.parse(value!);
|
||||
},
|
||||
initialValue:
|
||||
widget.initialDateTime.day.toString().padLeft(2, "0"),
|
||||
|
@ -107,7 +108,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
AppLocalizations.of(context).timeSubtitle,
|
||||
AppLocalizations.of(context)!.timeSubtitle,
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
),
|
||||
Row(
|
||||
|
@ -116,18 +117,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
AppLocalizations.of(context).timeHourInputHint,
|
||||
AppLocalizations.of(context)!.timeHourInputHint,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (int.tryParse(value)?.inRange(0, 23) == true) {
|
||||
if (int.tryParse(value!)?.inRange(0, 23) == true) {
|
||||
return null;
|
||||
}
|
||||
return AppLocalizations.of(context)
|
||||
return AppLocalizations.of(context)!
|
||||
.dateTimeInputInvalid;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.hour = int.parse(value);
|
||||
_formValue.hour = int.parse(value!);
|
||||
},
|
||||
initialValue: widget.initialDateTime.hour
|
||||
.toString()
|
||||
|
@ -140,18 +141,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
AppLocalizations.of(context).timeMinuteInputHint,
|
||||
AppLocalizations.of(context)!.timeMinuteInputHint,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (int.tryParse(value)?.inRange(0, 59) == true) {
|
||||
if (int.tryParse(value!)?.inRange(0, 59) == true) {
|
||||
return null;
|
||||
}
|
||||
return AppLocalizations.of(context)
|
||||
return AppLocalizations.of(context)!
|
||||
.dateTimeInputInvalid;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.minute = int.parse(value);
|
||||
_formValue.minute = int.parse(value!);
|
||||
},
|
||||
initialValue: widget.initialDateTime.minute
|
||||
.toString()
|
||||
|
@ -180,8 +181,8 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
}
|
||||
|
||||
void _onSavePressed(BuildContext context) {
|
||||
if (_formKey.currentState.validate()) {
|
||||
_formKey.currentState.save();
|
||||
if (_formKey.currentState?.validate() == true) {
|
||||
_formKey.currentState!.save();
|
||||
final d = DateTime(_formValue.year, _formValue.month, _formValue.day,
|
||||
_formValue.hour, _formValue.minute);
|
||||
_log.info("[_onSavePressed] Set date time: $d");
|
||||
|
@ -197,9 +198,9 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
|||
}
|
||||
|
||||
class _FormValue {
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
int hour;
|
||||
int minute;
|
||||
late int year;
|
||||
late int month;
|
||||
late int day;
|
||||
late int hour;
|
||||
late int minute;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import 'package:nc_photos/theme.dart';
|
|||
|
||||
class PhotoListImage extends StatelessWidget {
|
||||
const PhotoListImage({
|
||||
Key key,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
this.isGif = false,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -72,9 +72,9 @@ class PhotoListImage extends StatelessWidget {
|
|||
|
||||
class PhotoListVideo extends StatelessWidget {
|
||||
const PhotoListVideo({
|
||||
Key key,
|
||||
@required this.account,
|
||||
@required this.previewUrl,
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.previewUrl,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
|
@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class PopupMenuZoom extends PopupMenuEntry<void> {
|
||||
PopupMenuZoom({
|
||||
Key key,
|
||||
@required this.initialValue,
|
||||
@required this.minValue,
|
||||
@required this.maxValue,
|
||||
Key? key,
|
||||
required this.initialValue,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -22,7 +22,7 @@ class PopupMenuZoom extends PopupMenuEntry<void> {
|
|||
final int initialValue;
|
||||
final double minValue;
|
||||
final double maxValue;
|
||||
final void Function(double) onChanged;
|
||||
final void Function(double)? onChanged;
|
||||
}
|
||||
|
||||
class _PopupMenuZoomState extends State<PopupMenuZoom> {
|
||||
|
|
|
@ -3,8 +3,8 @@ import 'package:flutter/widgets.dart';
|
|||
|
||||
class ProcessingDialog extends StatelessWidget {
|
||||
ProcessingDialog({
|
||||
Key key,
|
||||
@required this.text,
|
||||
Key? key,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
|
@ -25,12 +25,17 @@ class RootPickerArguments {
|
|||
class RootPicker extends StatefulWidget {
|
||||
static const routeName = "/root-picker";
|
||||
|
||||
static Route buildRoute(RootPickerArguments args) =>
|
||||
MaterialPageRoute<Account>(
|
||||
builder: (context) => RootPicker.fromArgs(args),
|
||||
);
|
||||
|
||||
RootPicker({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
RootPicker.fromArgs(RootPickerArguments args, {Key key})
|
||||
RootPicker.fromArgs(RootPickerArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -100,7 +105,7 @@ class _RootPickerState extends State<RootPicker>
|
|||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).rootPickerHeaderText,
|
||||
AppLocalizations.of(context)!.rootPickerHeaderText,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
@ -108,7 +113,7 @@ class _RootPickerState extends State<RootPicker>
|
|||
Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).rootPickerSubHeaderText,
|
||||
AppLocalizations.of(context)!.rootPickerSubHeaderText,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -127,11 +132,11 @@ class _RootPickerState extends State<RootPicker>
|
|||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _onSkipPressed(context),
|
||||
child: Text(AppLocalizations.of(context).skipButtonLabel),
|
||||
child: Text(AppLocalizations.of(context)!.skipButtonLabel),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => _onConfirmPressed(context),
|
||||
child: Text(AppLocalizations.of(context).confirmButtonLabel),
|
||||
child: Text(AppLocalizations.of(context)!.confirmButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -145,7 +150,7 @@ class _RootPickerState extends State<RootPicker>
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.rootPickerSkipConfirmationDialogContent),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
|
@ -174,7 +179,7 @@ class _RootPickerState extends State<RootPicker>
|
|||
if (roots.isEmpty) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).rootPickerListEmptyNotification),
|
||||
Text(AppLocalizations.of(context)!.rootPickerListEmptyNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
|
@ -189,12 +194,12 @@ class _RootPickerState extends State<RootPicker>
|
|||
return;
|
||||
}
|
||||
_isInitDialogShown = true;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
SchedulerBinding.instance!.addPostFrameCallback((_) {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => ProcessingDialog(
|
||||
text: AppLocalizations.of(context).genericProcessingDialogContent),
|
||||
text: AppLocalizations.of(context)!.genericProcessingDialogContent),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ import 'package:nc_photos/theme.dart';
|
|||
// Overlay a check mark if an item is selected
|
||||
class Selectable extends StatelessWidget {
|
||||
Selectable({
|
||||
Key key,
|
||||
@required this.child,
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.isSelected = false,
|
||||
@required this.iconSize,
|
||||
required this.iconSize,
|
||||
this.borderRadius,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
|
@ -55,8 +55,8 @@ class Selectable extends StatelessWidget {
|
|||
final Widget child;
|
||||
final bool isSelected;
|
||||
final double iconSize;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import 'package:nc_photos/widget/selectable.dart';
|
|||
abstract class SelectableItem {
|
||||
Widget buildWidget(BuildContext context);
|
||||
|
||||
VoidCallback get onTap => null;
|
||||
VoidCallback? get onTap => null;
|
||||
bool get isSelectable => false;
|
||||
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
@protected
|
||||
Widget buildItemStreamListOuter(
|
||||
BuildContext context, {
|
||||
@required Widget child,
|
||||
required Widget child,
|
||||
}) {
|
||||
if (platform_k.isWeb) {
|
||||
// support shift+click group selection on web
|
||||
|
@ -51,8 +51,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
|
||||
@protected
|
||||
Widget buildItemStreamList({
|
||||
@required double maxCrossAxisExtent,
|
||||
ValueChanged<double> onMaxExtentChanged,
|
||||
required double maxCrossAxisExtent,
|
||||
ValueChanged<double?>? onMaxExtentChanged,
|
||||
}) {
|
||||
return MeasurableItemList(
|
||||
key: _listKey,
|
||||
|
@ -81,24 +81,25 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
@protected
|
||||
set itemStreamListItems(List<SelectableItem> newItems) {
|
||||
final lastSelectedItem =
|
||||
_lastSelectPosition != null ? _items[_lastSelectPosition] : null;
|
||||
_lastSelectPosition != null ? _items[_lastSelectPosition!] : null;
|
||||
|
||||
_items = newItems;
|
||||
_transformSelectedItems();
|
||||
|
||||
// Keep _lastSelectPosition if no changes, drop otherwise
|
||||
int newLastSelectPosition;
|
||||
int? newLastSelectPosition;
|
||||
try {
|
||||
if (lastSelectedItem != null &&
|
||||
lastSelectedItem == _items[_lastSelectPosition]) {
|
||||
newLastSelectPosition = _lastSelectPosition;
|
||||
lastSelectedItem == _items[_lastSelectPosition!]) {
|
||||
newLastSelectPosition = _lastSelectPosition!;
|
||||
}
|
||||
} catch (_) {}
|
||||
_lastSelectPosition = newLastSelectPosition;
|
||||
|
||||
_log.info("[itemStreamListItems] updateListHeight: list item changed");
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||
(_listKey.currentState as MeasurableItemListState)?.updateListHeight());
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) =>
|
||||
(_listKey.currentState as MeasurableItemListState?)
|
||||
?.updateListHeight());
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, int index) {
|
||||
|
@ -139,8 +140,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
if (_isRangeSelectionMode && _lastSelectPosition != null) {
|
||||
setState(() {
|
||||
_selectedItems.addAll(_items
|
||||
.sublist(math.min(_lastSelectPosition, index),
|
||||
math.max(_lastSelectPosition, index) + 1)
|
||||
.sublist(math.min(_lastSelectPosition!, index),
|
||||
math.max(_lastSelectPosition!, index) + 1)
|
||||
.where((e) => e.isSelectable));
|
||||
_lastSelectPosition = index;
|
||||
});
|
||||
|
@ -166,8 +167,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
if (!platform_k.isWeb && wasSelectionMode && _lastSelectPosition != null) {
|
||||
setState(() {
|
||||
_selectedItems.addAll(_items
|
||||
.sublist(math.min(_lastSelectPosition, index),
|
||||
math.max(_lastSelectPosition, index) + 1)
|
||||
.sublist(math.min(_lastSelectPosition!, index),
|
||||
math.max(_lastSelectPosition!, index) + 1)
|
||||
.where((e) => e.isSelectable));
|
||||
_lastSelectPosition = index;
|
||||
});
|
||||
|
@ -183,8 +184,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
if (!SessionStorage().hasShowRangeSelectNotification) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(platform_k.isWeb
|
||||
? AppLocalizations.of(context).webSelectRangeNotification
|
||||
: AppLocalizations.of(context).mobileSelectRangeNotification),
|
||||
? AppLocalizations.of(context)!.webSelectRangeNotification
|
||||
: AppLocalizations.of(context)!.mobileSelectRangeNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
SessionStorage().hasShowRangeSelectNotification = true;
|
||||
|
@ -205,14 +206,14 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
return null;
|
||||
}
|
||||
})
|
||||
.where((element) => element != null)
|
||||
.whereType<SelectableItem>()
|
||||
.toList();
|
||||
_selectedItems
|
||||
..clear()
|
||||
..addAll(newSelectedItems);
|
||||
}
|
||||
|
||||
int _lastSelectPosition;
|
||||
int? _lastSelectPosition;
|
||||
bool _isRangeSelectionMode = false;
|
||||
|
||||
var _items = <SelectableItem>[];
|
||||
|
|
|
@ -23,12 +23,16 @@ class SettingsArguments {
|
|||
class Settings extends StatefulWidget {
|
||||
static const routeName = "/settings";
|
||||
|
||||
static Route buildRoute(SettingsArguments args) => MaterialPageRoute(
|
||||
builder: (context) => Settings.fromArgs(args),
|
||||
);
|
||||
|
||||
Settings({
|
||||
Key key,
|
||||
@required this.account,
|
||||
Key? key,
|
||||
required this.account,
|
||||
}) : super(key: key);
|
||||
|
||||
Settings.fromArgs(SettingsArguments args, {Key key})
|
||||
Settings.fromArgs(SettingsArguments args, {Key? key})
|
||||
: this(
|
||||
account: args.account,
|
||||
);
|
||||
|
@ -43,7 +47,7 @@ class _SettingsState extends State<Settings> {
|
|||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_isEnableExif = Pref.inst().isEnableExif();
|
||||
_isEnableExif = Pref.inst().isEnableExifOr();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -58,40 +62,41 @@ class _SettingsState extends State<Settings> {
|
|||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
final translator = AppLocalizations.of(context).translator;
|
||||
final translator = AppLocalizations.of(context)!.translator;
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
title: Text(AppLocalizations.of(context).settingsWidgetTitle),
|
||||
title: Text(AppLocalizations.of(context)!.settingsWidgetTitle),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
ListTile(
|
||||
title: Text(AppLocalizations.of(context).settingsLanguageTitle),
|
||||
title:
|
||||
Text(AppLocalizations.of(context)!.settingsLanguageTitle),
|
||||
subtitle: Text(language_util.getSelectedLanguageName(context)),
|
||||
onTap: () => _onLanguageTap(context),
|
||||
),
|
||||
SwitchListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context).settingsExifSupportTitle),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.settingsExifSupportTitle),
|
||||
subtitle: _isEnableExif
|
||||
? Text(AppLocalizations.of(context)
|
||||
? Text(AppLocalizations.of(context)!
|
||||
.settingsExifSupportTrueSubtitle)
|
||||
: null,
|
||||
value: _isEnableExif,
|
||||
onChanged: (value) => _onExifSupportChanged(context, value),
|
||||
),
|
||||
_buildCaption(context,
|
||||
AppLocalizations.of(context).settingsAboutSectionTitle),
|
||||
AppLocalizations.of(context)!.settingsAboutSectionTitle),
|
||||
ListTile(
|
||||
title: Text(AppLocalizations.of(context).settingsVersionTitle),
|
||||
title: Text(AppLocalizations.of(context)!.settingsVersionTitle),
|
||||
subtitle: const Text(k.versionStr),
|
||||
),
|
||||
ListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context).settingsSourceCodeTitle),
|
||||
Text(AppLocalizations.of(context)!.settingsSourceCodeTitle),
|
||||
subtitle: Text(_sourceRepo),
|
||||
onTap: () async {
|
||||
await launch(_sourceRepo);
|
||||
|
@ -99,7 +104,7 @@ class _SettingsState extends State<Settings> {
|
|||
),
|
||||
ListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context).settingsBugReportTitle),
|
||||
Text(AppLocalizations.of(context)!.settingsBugReportTitle),
|
||||
onTap: () {
|
||||
launch(_bugReportUrl);
|
||||
},
|
||||
|
@ -107,7 +112,7 @@ class _SettingsState extends State<Settings> {
|
|||
if (translator.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).settingsTranslatorTitle),
|
||||
AppLocalizations.of(context)!.settingsTranslatorTitle),
|
||||
subtitle: Text(translator),
|
||||
onTap: () {
|
||||
launch(_translationUrl);
|
||||
|
@ -172,8 +177,8 @@ class _SettingsState extends State<Settings> {
|
|||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).exifSupportConfirmationDialogTitle),
|
||||
content: Text(AppLocalizations.of(context).exifSupportDetails),
|
||||
AppLocalizations.of(context)!.exifSupportConfirmationDialogTitle),
|
||||
content: Text(AppLocalizations.of(context)!.exifSupportDetails),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
|
@ -185,7 +190,7 @@ class _SettingsState extends State<Settings> {
|
|||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).enableButtonLabel),
|
||||
child: Text(AppLocalizations.of(context)!.enableButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -215,7 +220,7 @@ class _SettingsState extends State<Settings> {
|
|||
_log.severe("[_setExifSupport] Failed writing pref");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).writePreferenceFailureNotification),
|
||||
AppLocalizations.of(context)!.writePreferenceFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
setState(() {
|
||||
|
@ -231,7 +236,7 @@ class _SettingsState extends State<Settings> {
|
|||
static const String _translationUrl =
|
||||
"https://gitlab.com/nkming2/nc-photos/-/tree/master/lib/l10n";
|
||||
|
||||
bool _isEnableExif;
|
||||
late bool _isEnableExif;
|
||||
|
||||
static final _log = Logger("widget.settings._SettingsState");
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import 'package:nc_photos/widget/sign_in.dart';
|
|||
import 'package:page_view_indicators/circle_page_indicator.dart';
|
||||
|
||||
bool isNeedSetup() =>
|
||||
Pref.inst().getSetupProgress() & _PageId.all != _PageId.all;
|
||||
Pref.inst().getSetupProgressOr() & _PageId.all != _PageId.all;
|
||||
|
||||
class Setup extends StatefulWidget {
|
||||
static const routeName = "/setup";
|
||||
|
@ -29,15 +29,15 @@ class _SetupState extends State<Setup> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context) {
|
||||
PreferredSizeWidget _buildAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text(AppLocalizations.of(context).setupWidgetTitle),
|
||||
title: Text(AppLocalizations.of(context)!.setupWidgetTitle),
|
||||
elevation: 0,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
final page = _pageController.hasClients ? _pageController.page.round() : 0;
|
||||
final page = _pageController.hasClients ? _pageController.page!.round() : 0;
|
||||
final pages = <Widget>[
|
||||
if (_initialProgress & _PageId.exif == 0) _Exif(),
|
||||
if (_initialProgress & _PageId.hiddenPrefDirNotice == 0)
|
||||
|
@ -70,16 +70,21 @@ class _SetupState extends State<Setup> {
|
|||
ElevatedButton(
|
||||
onPressed: _onDonePressed,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).doneButtonLabel),
|
||||
AppLocalizations.of(context)!.doneButtonLabel),
|
||||
),
|
||||
]
|
||||
: [
|
||||
ElevatedButton(
|
||||
onPressed: () => _onNextPressed(
|
||||
(pages[_pageController.page.round()] as _Page)
|
||||
.getPageId()),
|
||||
onPressed: () {
|
||||
if (_pageController.hasClients) {
|
||||
_onNextPressed(
|
||||
(pages[_pageController.page!.round()]
|
||||
as _Page)
|
||||
.getPageId());
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).nextButtonLabel),
|
||||
AppLocalizations.of(context)!.nextButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -107,12 +112,12 @@ class _SetupState extends State<Setup> {
|
|||
}
|
||||
|
||||
void _onNextPressed(int pageId) {
|
||||
Pref.inst().setSetupProgress(Pref.inst().getSetupProgress() | pageId);
|
||||
Pref.inst().setSetupProgress(Pref.inst().getSetupProgressOr() | pageId);
|
||||
_pageController.nextPage(
|
||||
duration: k.animationDurationNormal, curve: Curves.easeInOut);
|
||||
}
|
||||
|
||||
final _initialProgress = Pref.inst().getSetupProgress();
|
||||
final _initialProgress = Pref.inst().getSetupProgressOr();
|
||||
final _pageController = PageController();
|
||||
var _currentPageNotifier = ValueNotifier<int>(0);
|
||||
}
|
||||
|
@ -143,23 +148,23 @@ class _ExifState extends State<_Exif> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context).settingsExifSupportTitle),
|
||||
title: Text(AppLocalizations.of(context)!.settingsExifSupportTitle),
|
||||
value: _isEnableExif,
|
||||
onChanged: _onValueChanged,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(AppLocalizations.of(context).exifSupportDetails),
|
||||
child: Text(AppLocalizations.of(context)!.exifSupportDetails),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).setupSettingsModifyLaterHint,
|
||||
AppLocalizations.of(context)!.setupSettingsModifyLaterHint,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText2
|
||||
.bodyText2!
|
||||
.copyWith(fontStyle: FontStyle.italic)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
@ -175,7 +180,7 @@ class _ExifState extends State<_Exif> {
|
|||
});
|
||||
}
|
||||
|
||||
bool _isEnableExif = Pref.inst().isEnableExif();
|
||||
bool _isEnableExif = Pref.inst().isEnableExifOr();
|
||||
}
|
||||
|
||||
class _HiddenPrefDirNotice extends StatefulWidget implements _Page {
|
||||
|
@ -196,7 +201,7 @@ class _HiddenPrefDirNoticeState extends State<_HiddenPrefDirNotice> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).setupHiddenPrefDirNoticeDetail),
|
||||
AppLocalizations.of(context)!.setupHiddenPrefDirNoticeDetail),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Align(
|
||||
|
|
|
@ -16,7 +16,7 @@ import 'package:nc_photos/widget/root_picker.dart';
|
|||
class SignIn extends StatefulWidget {
|
||||
static const routeName = "/sign-in";
|
||||
|
||||
SignIn({Key key}) : super(key: key);
|
||||
SignIn({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
createState() => _SignInState();
|
||||
|
@ -49,7 +49,7 @@ class _SignInState extends State<SignIn> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).signInHeaderText,
|
||||
AppLocalizations.of(context)!.signInHeaderText,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
@ -70,7 +70,7 @@ class _SignInState extends State<SignIn> {
|
|||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32, vertical: 16),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).signIn2faHintText,
|
||||
AppLocalizations.of(context)!.signIn2faHintText,
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
),
|
||||
|
@ -82,7 +82,7 @@ class _SignInState extends State<SignIn> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (!ModalRoute.of(context).isFirst)
|
||||
if (!ModalRoute.of(context)!.isFirst)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
|
@ -94,11 +94,11 @@ class _SignInState extends State<SignIn> {
|
|||
Container(),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState.validate()) {
|
||||
if (_formKey.currentState?.validate() == true) {
|
||||
_connect();
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.connectButtonLabel),
|
||||
),
|
||||
],
|
||||
|
@ -143,11 +143,11 @@ class _SignInState extends State<SignIn> {
|
|||
.toList(),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
_scheme = newValue;
|
||||
_scheme = newValue!;
|
||||
});
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.scheme = value.toValueString();
|
||||
_formValue.scheme = value!.toValueString();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -159,18 +159,19 @@ class _SignInState extends State<SignIn> {
|
|||
Expanded(
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context).serverAddressInputHint,
|
||||
hintText:
|
||||
AppLocalizations.of(context)!.serverAddressInputHint,
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value.trim().trimRightAny("/").isEmpty) {
|
||||
return AppLocalizations.of(context)
|
||||
if (value!.trim().trimRightAny("/").isEmpty) {
|
||||
return AppLocalizations.of(context)!
|
||||
.serverAddressInputInvalidEmpty;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.address = value.trim().trimRightAny("/");
|
||||
_formValue.address = value!.trim().trimRightAny("/");
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -179,32 +180,32 @@ class _SignInState extends State<SignIn> {
|
|||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context).usernameInputHint,
|
||||
hintText: AppLocalizations.of(context)!.usernameInputHint,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return AppLocalizations.of(context).usernameInputInvalidEmpty;
|
||||
if (value!.trim().isEmpty) {
|
||||
return AppLocalizations.of(context)!.usernameInputInvalidEmpty;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.username = value;
|
||||
_formValue.username = value!;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context).passwordInputHint,
|
||||
hintText: AppLocalizations.of(context)!.passwordInputHint,
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return AppLocalizations.of(context).passwordInputInvalidEmpty;
|
||||
if (value!.trim().isEmpty) {
|
||||
return AppLocalizations.of(context)!.passwordInputInvalidEmpty;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_formValue.password = value;
|
||||
_formValue.password = value!;
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -212,22 +213,23 @@ class _SignInState extends State<SignIn> {
|
|||
}
|
||||
|
||||
void _connect() {
|
||||
_formKey.currentState.save();
|
||||
_formKey.currentState!.save();
|
||||
final account = Account(_formValue.scheme, _formValue.address,
|
||||
_formValue.username, _formValue.password, [""]);
|
||||
_log.info("[_connect] Try connecting with account: $account");
|
||||
Navigator.pushNamed(context, Connect.routeName,
|
||||
Navigator.pushNamed<Account>(context, Connect.routeName,
|
||||
arguments: ConnectArguments(account))
|
||||
.then((result) {
|
||||
.then<Account?>((result) {
|
||||
return result != null
|
||||
? Navigator.pushNamed(context, RootPicker.routeName,
|
||||
arguments: RootPickerArguments(result))
|
||||
: null;
|
||||
: Future.value(null);
|
||||
}).then((result) {
|
||||
if (result != null) {
|
||||
// we've got a good account
|
||||
// only signing in with app password would trigger distinct
|
||||
final accounts = (Pref.inst().getAccounts([])..add(result)).distinct();
|
||||
final accounts =
|
||||
(Pref.inst().getAccountsOr([])..add(result)).distinct();
|
||||
Pref.inst()
|
||||
..setAccounts(accounts)
|
||||
..setCurrentAccountIndex(accounts.indexOf(result));
|
||||
|
@ -267,8 +269,8 @@ extension on _Scheme {
|
|||
}
|
||||
|
||||
class _FormValue {
|
||||
String scheme;
|
||||
String address;
|
||||
String username;
|
||||
String password;
|
||||
late String scheme;
|
||||
late String address;
|
||||
late String username;
|
||||
late String password;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
|
|||
|
||||
class SimpleInputDialog extends StatefulWidget {
|
||||
SimpleInputDialog({
|
||||
Key key,
|
||||
Key? key,
|
||||
this.initialText,
|
||||
this.hintText,
|
||||
this.validator,
|
||||
|
@ -12,9 +12,9 @@ class SimpleInputDialog extends StatefulWidget {
|
|||
@override
|
||||
createState() => _SimpleInputDialogState();
|
||||
|
||||
final String initialText;
|
||||
final String hintText;
|
||||
final FormFieldValidator<String> validator;
|
||||
final String? initialText;
|
||||
final String? hintText;
|
||||
final FormFieldValidator<String>? validator;
|
||||
}
|
||||
|
||||
class _SimpleInputDialogState extends State<SimpleInputDialog> {
|
||||
|
@ -29,7 +29,7 @@ class _SimpleInputDialogState extends State<SimpleInputDialog> {
|
|||
: InputDecoration(hintText: widget.hintText),
|
||||
validator: widget.validator,
|
||||
onSaved: (value) {
|
||||
_formValue.text = value;
|
||||
_formValue.text = value!;
|
||||
},
|
||||
initialValue: widget.initialText,
|
||||
),
|
||||
|
@ -48,15 +48,15 @@ class _SimpleInputDialogState extends State<SimpleInputDialog> {
|
|||
void _onSavePressed() {
|
||||
if (_formKey.currentState?.validate() == true) {
|
||||
_formValue = _FormValue();
|
||||
_formKey.currentState.save();
|
||||
_formKey.currentState!.save();
|
||||
Navigator.of(context).pop(_formValue.text);
|
||||
}
|
||||
}
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
_FormValue _formValue;
|
||||
var _formValue = _FormValue();
|
||||
}
|
||||
|
||||
class _FormValue {
|
||||
String text;
|
||||
late String text;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import 'package:nc_photos/widget/sign_in.dart';
|
|||
class Splash extends StatefulWidget {
|
||||
static const routeName = "/splash";
|
||||
|
||||
Splash({Key key}) : super(key: key);
|
||||
Splash({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
createState() => _SplashState();
|
||||
|
@ -23,7 +23,7 @@ class _SplashState extends State<Splash> {
|
|||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
||||
if (_shouldUpgrade()) {
|
||||
_handleUpgrade();
|
||||
} else {
|
||||
|
@ -56,7 +56,7 @@ class _SplashState extends State<Splash> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context).appTitle,
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
)
|
||||
|
@ -81,12 +81,12 @@ class _SplashState extends State<Splash> {
|
|||
}
|
||||
|
||||
bool _shouldUpgrade() {
|
||||
final lastVersion = Pref.inst().getLastVersion(k.version);
|
||||
final lastVersion = Pref.inst().getLastVersionOr(k.version);
|
||||
return lastVersion < k.version;
|
||||
}
|
||||
|
||||
void _handleUpgrade() {
|
||||
final lastVersion = Pref.inst().getLastVersion(k.version);
|
||||
final lastVersion = Pref.inst().getLastVersionOr(k.version);
|
||||
// ...
|
||||
|
||||
final change = _gatherChangelog(lastVersion);
|
||||
|
@ -94,7 +94,7 @@ class _SplashState extends State<Splash> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).changelogTitle),
|
||||
title: Text(AppLocalizations.of(context)!.changelogTitle),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(change),
|
||||
),
|
||||
|
|
|
@ -12,8 +12,8 @@ import 'package:wakelock/wakelock.dart';
|
|||
|
||||
class VideoViewer extends StatefulWidget {
|
||||
VideoViewer({
|
||||
@required this.account,
|
||||
@required this.file,
|
||||
required this.account,
|
||||
required this.file,
|
||||
this.onLoaded,
|
||||
this.onHeightChanged,
|
||||
this.onPlay,
|
||||
|
@ -27,10 +27,10 @@ class VideoViewer extends StatefulWidget {
|
|||
|
||||
final Account account;
|
||||
final File file;
|
||||
final VoidCallback onLoaded;
|
||||
final void Function(double height) onHeightChanged;
|
||||
final VoidCallback onPlay;
|
||||
final VoidCallback onPause;
|
||||
final VoidCallback? onLoaded;
|
||||
final ValueChanged<double>? onHeightChanged;
|
||||
final VoidCallback? onPlay;
|
||||
final VoidCallback? onPause;
|
||||
final bool isControlVisible;
|
||||
final bool canPlay;
|
||||
}
|
||||
|
@ -47,9 +47,9 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
)..initialize().then((_) {
|
||||
widget.onLoaded?.call();
|
||||
setState(() {});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
if (_key.currentContext != null) {
|
||||
widget.onHeightChanged?.call(_key.currentContext.size.height);
|
||||
widget.onHeightChanged?.call(_key.currentContext!.size!.height);
|
||||
}
|
||||
});
|
||||
}).catchError((e, stacktrace) {
|
||||
|
@ -80,13 +80,13 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
@override
|
||||
dispose() {
|
||||
super.dispose();
|
||||
_controller?.dispose();
|
||||
_controller.dispose();
|
||||
Wakelock.disable();
|
||||
}
|
||||
|
||||
Widget _buildPlayer(BuildContext context) {
|
||||
if (_controller.value.isPlaying && !widget.canPlay) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
_pause();
|
||||
});
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
}
|
||||
|
||||
final _key = GlobalKey();
|
||||
VideoPlayerController _controller;
|
||||
late VideoPlayerController _controller;
|
||||
var _isFinished = false;
|
||||
|
||||
static final _log = Logger("widget.video_viewer._VideoViewerState");
|
||||
|
|
|
@ -38,14 +38,18 @@ class ViewerArguments {
|
|||
class Viewer extends StatefulWidget {
|
||||
static const routeName = "/viewer";
|
||||
|
||||
static Route buildRoute(ViewerArguments args) => MaterialPageRoute(
|
||||
builder: (context) => Viewer.fromArgs(args),
|
||||
);
|
||||
|
||||
Viewer({
|
||||
Key key,
|
||||
@required this.account,
|
||||
@required this.streamFiles,
|
||||
@required this.startIndex,
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.streamFiles,
|
||||
required this.startIndex,
|
||||
}) : super(key: key);
|
||||
|
||||
Viewer.fromArgs(ViewerArguments args, {Key key})
|
||||
Viewer.fromArgs(ViewerArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
|
@ -124,7 +128,7 @@ class _ViewerState extends State<Viewer> {
|
|||
children: [
|
||||
Container(color: Colors.black),
|
||||
if (!_pageController.hasClients ||
|
||||
!_pageStates[_pageController.page.round()].hasLoaded)
|
||||
!_pageStates[_pageController.page!.round()]!.hasLoaded)
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: const CircularProgressIndicator(),
|
||||
|
@ -260,7 +264,7 @@ class _ViewerState extends State<Viewer> {
|
|||
if (!_isDetailPaneActive && _canOpenDetailPane())
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
tooltip: AppLocalizations.of(context).detailsTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.detailsTooltip,
|
||||
onPressed: _onDetailsPressed,
|
||||
),
|
||||
],
|
||||
|
@ -307,7 +311,7 @@ class _ViewerState extends State<Viewer> {
|
|||
Icons.share_outlined,
|
||||
color: Colors.white.withOpacity(.87),
|
||||
),
|
||||
tooltip: AppLocalizations.of(context).shareTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.shareTooltip,
|
||||
onPressed: () => _onSharePressed(context),
|
||||
),
|
||||
),
|
||||
|
@ -318,7 +322,7 @@ class _ViewerState extends State<Viewer> {
|
|||
Icons.download_outlined,
|
||||
color: Colors.white.withOpacity(.87),
|
||||
),
|
||||
tooltip: AppLocalizations.of(context).downloadTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.downloadTooltip,
|
||||
onPressed: () => _onDownloadPressed(context),
|
||||
),
|
||||
),
|
||||
|
@ -329,7 +333,7 @@ class _ViewerState extends State<Viewer> {
|
|||
Icons.delete_outlined,
|
||||
color: Colors.white.withOpacity(.87),
|
||||
),
|
||||
tooltip: AppLocalizations.of(context).deleteTooltip,
|
||||
tooltip: AppLocalizations.of(context)!.deleteTooltip,
|
||||
onPressed: () => _onDeletePressed(context),
|
||||
),
|
||||
),
|
||||
|
@ -344,7 +348,7 @@ class _ViewerState extends State<Viewer> {
|
|||
Widget _buildPage(BuildContext context, int index) {
|
||||
if (_pageStates[index] == null) {
|
||||
_onCreateNewPage(context, index);
|
||||
} else if (!_pageStates[index].scrollController.hasClients) {
|
||||
} else if (!_pageStates[index]!.scrollController.hasClients) {
|
||||
// the page has been moved out of view and is now coming back
|
||||
_log.fine("[_buildPage] Recreating page#$index");
|
||||
_onRecreatePageAfterMovedOut(context, index);
|
||||
|
@ -359,7 +363,7 @@ class _ViewerState extends State<Viewer> {
|
|||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (notif) => _onPageContentScrolled(notif, index),
|
||||
child: SingleChildScrollView(
|
||||
controller: _pageStates[index].scrollController,
|
||||
controller: _pageStates[index]!.scrollController,
|
||||
physics:
|
||||
_isDetailPaneActive ? null : const NeverScrollableScrollPhysics(),
|
||||
child: Stack(
|
||||
|
@ -409,7 +413,7 @@ class _ViewerState extends State<Viewer> {
|
|||
return _buildVideoView(context, index);
|
||||
} else {
|
||||
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}");
|
||||
_pageStates[index].itemHeight = 0;
|
||||
_pageStates[index]!.itemHeight = 0;
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
@ -452,7 +456,7 @@ class _ViewerState extends State<Viewer> {
|
|||
return false;
|
||||
}
|
||||
if (notification is ScrollEndNotification) {
|
||||
final scrollPos = _pageStates[index].scrollController.position;
|
||||
final scrollPos = _pageStates[index]!.scrollController.position;
|
||||
if (scrollPos.pixels == 0) {
|
||||
setState(() {
|
||||
_onDetailPaneClosed();
|
||||
|
@ -463,14 +467,15 @@ class _ViewerState extends State<Viewer> {
|
|||
// upward, open the pane to its minimal size
|
||||
Future.delayed(Duration.zero, () {
|
||||
setState(() {
|
||||
_openDetailPane(_pageController.page.toInt(),
|
||||
_openDetailPane(_pageController.page!.toInt(),
|
||||
shouldAnimate: true);
|
||||
});
|
||||
});
|
||||
} else if (scrollPos.userScrollDirection == ScrollDirection.forward) {
|
||||
// downward, close the pane
|
||||
Future.delayed(Duration.zero, () {
|
||||
_closeDetailPane(_pageController.page.toInt(), shouldAnimate: true);
|
||||
_closeDetailPane(_pageController.page!.toInt(),
|
||||
shouldAnimate: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -481,8 +486,8 @@ class _ViewerState extends State<Viewer> {
|
|||
void _onImageLoaded(int index) {
|
||||
// currently pageview doesn't pre-load pages, we do it manually
|
||||
// don't pre-load if user already navigated away
|
||||
if (_pageController.page.round() == index &&
|
||||
!_pageStates[index].hasLoaded) {
|
||||
if (_pageController.page!.round() == index &&
|
||||
!_pageStates[index]!.hasLoaded) {
|
||||
_log.info("[_onImageLoaded] Pre-loading nearby images");
|
||||
if (index > 0) {
|
||||
final prevFile = widget.streamFiles[index - 1];
|
||||
|
@ -497,16 +502,16 @@ class _ViewerState extends State<Viewer> {
|
|||
}
|
||||
}
|
||||
setState(() {
|
||||
_pageStates[index].hasLoaded = true;
|
||||
_pageStates[index]!.hasLoaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onVideoLoaded(int index) {
|
||||
if (_pageController.page.round() == index &&
|
||||
!_pageStates[index].hasLoaded) {
|
||||
if (_pageController.page!.round() == index &&
|
||||
!_pageStates[index]!.hasLoaded) {
|
||||
setState(() {
|
||||
_pageStates[index].hasLoaded = true;
|
||||
_pageStates[index]!.hasLoaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -534,16 +539,16 @@ class _ViewerState extends State<Viewer> {
|
|||
/// Called when the page is being built after previously moved out of view
|
||||
void _onRecreatePageAfterMovedOut(BuildContext context, int index) {
|
||||
if (_isShowDetailPane && !_isClosingDetailPane) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pageStates[index].itemHeight != null) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
if (_pageStates[index]!.itemHeight != null) {
|
||||
setState(() {
|
||||
_openDetailPane(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_pageStates[index].scrollController.jumpTo(0);
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
_pageStates[index]!.scrollController.jumpTo(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -551,26 +556,26 @@ class _ViewerState extends State<Viewer> {
|
|||
void _onDetailsPressed() {
|
||||
if (!_isDetailPaneActive) {
|
||||
setState(() {
|
||||
_openDetailPane(_pageController.page.toInt(), shouldAnimate: true);
|
||||
_openDetailPane(_pageController.page!.toInt(), shouldAnimate: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onSharePressed(BuildContext context) {
|
||||
assert(platform_k.isAndroid);
|
||||
final file = widget.streamFiles[_pageController.page.round()];
|
||||
final file = widget.streamFiles[_pageController.page!.round()];
|
||||
ShareHandler().shareFiles(context, widget.account, [file]);
|
||||
}
|
||||
|
||||
void _onDownloadPressed(BuildContext context) async {
|
||||
final file = widget.streamFiles[_pageController.page.round()];
|
||||
final file = widget.streamFiles[_pageController.page!.round()];
|
||||
_log.info("[_onDownloadPressed] Downloading file: ${file.path}");
|
||||
var controller = SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).downloadProcessingNotification),
|
||||
Text(AppLocalizations.of(context)!.downloadProcessingNotification),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
controller?.closed?.whenComplete(() {
|
||||
controller?.closed.whenComplete(() {
|
||||
controller = null;
|
||||
});
|
||||
dynamic result;
|
||||
|
@ -581,7 +586,7 @@ class _ViewerState extends State<Viewer> {
|
|||
_log.warning("[_onDownloadPressed] Permission not granted");
|
||||
controller?.close();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.downloadFailureNoPermissionNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -591,9 +596,9 @@ class _ViewerState extends State<Viewer> {
|
|||
"[_onDownloadPressed] Failed while downloadFile", e, stacktrace);
|
||||
controller?.close();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text("${AppLocalizations.of(context).downloadFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
content: Text(
|
||||
"${AppLocalizations.of(context)!.downloadFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
|
@ -622,19 +627,19 @@ class _ViewerState extends State<Viewer> {
|
|||
|
||||
// fallback
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context).downloadSuccessNotification),
|
||||
content: Text(AppLocalizations.of(context)!.downloadSuccessNotification),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
}
|
||||
|
||||
void _onDeletePressed(BuildContext context) async {
|
||||
final file = widget.streamFiles[_pageController.page.round()];
|
||||
final file = widget.streamFiles[_pageController.page!.round()];
|
||||
_log.info("[_onDeletePressed] Removing file: ${file.path}");
|
||||
var controller = SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context).deleteProcessingNotification),
|
||||
content: Text(AppLocalizations.of(context)!.deleteProcessingNotification),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
controller?.closed?.whenComplete(() {
|
||||
controller?.closed.whenComplete(() {
|
||||
controller = null;
|
||||
});
|
||||
try {
|
||||
|
@ -642,7 +647,7 @@ class _ViewerState extends State<Viewer> {
|
|||
AlbumRepo(AlbumCachedDataSource()))(widget.account, file);
|
||||
controller?.close();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context).deleteSuccessNotification),
|
||||
content: Text(AppLocalizations.of(context)!.deleteSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
Navigator.of(context).pop();
|
||||
|
@ -655,7 +660,7 @@ class _ViewerState extends State<Viewer> {
|
|||
controller?.close();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text("${AppLocalizations.of(context).deleteFailureNotification}: "
|
||||
Text("${AppLocalizations.of(context)!.deleteFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -666,8 +671,9 @@ class _ViewerState extends State<Viewer> {
|
|||
if (_pageStates[index]?.itemHeight == null) {
|
||||
return MediaQuery.of(context).size.height;
|
||||
} else {
|
||||
return _pageStates[index].itemHeight +
|
||||
(MediaQuery.of(context).size.height - _pageStates[index].itemHeight) /
|
||||
return _pageStates[index]!.itemHeight! +
|
||||
(MediaQuery.of(context).size.height -
|
||||
_pageStates[index]!.itemHeight!) /
|
||||
2 -
|
||||
4;
|
||||
}
|
||||
|
@ -680,10 +686,10 @@ class _ViewerState extends State<Viewer> {
|
|||
}
|
||||
|
||||
void _updateItemHeight(int index, double height) {
|
||||
if (_pageStates[index].itemHeight != height) {
|
||||
if (_pageStates[index]!.itemHeight != height) {
|
||||
_log.fine("[_updateItemHeight] New height of item#$index: $height");
|
||||
setState(() {
|
||||
_pageStates[index].itemHeight = height;
|
||||
_pageStates[index]!.itemHeight = height;
|
||||
if (_isDetailPaneActive) {
|
||||
_openDetailPane(index);
|
||||
}
|
||||
|
@ -709,12 +715,12 @@ class _ViewerState extends State<Viewer> {
|
|||
_isShowDetailPane = true;
|
||||
_isDetailPaneActive = true;
|
||||
if (shouldAnimate) {
|
||||
_pageStates[index].scrollController.animateTo(
|
||||
_pageStates[index]!.scrollController.animateTo(
|
||||
_calcDetailPaneOpenedScrollPosition(index),
|
||||
duration: k.animationDurationNormal,
|
||||
curve: Curves.easeOut);
|
||||
} else {
|
||||
_pageStates[index]
|
||||
_pageStates[index]!
|
||||
.scrollController
|
||||
.jumpTo(_calcDetailPaneOpenedScrollPosition(index));
|
||||
}
|
||||
|
@ -723,7 +729,7 @@ class _ViewerState extends State<Viewer> {
|
|||
void _closeDetailPane(int index, {bool shouldAnimate = false}) {
|
||||
_isClosingDetailPane = true;
|
||||
if (shouldAnimate) {
|
||||
_pageStates[index].scrollController.animateTo(0,
|
||||
_pageStates[index]!.scrollController.animateTo(0,
|
||||
duration: k.animationDurationNormal, curve: Curves.easeOut);
|
||||
}
|
||||
}
|
||||
|
@ -739,7 +745,7 @@ class _ViewerState extends State<Viewer> {
|
|||
.previousPage(
|
||||
duration: k.animationDurationNormal, curve: Curves.easeInOut)
|
||||
.whenComplete(
|
||||
() => _updateNavigationState(_pageController.page.round()));
|
||||
() => _updateNavigationState(_pageController.page!.round()));
|
||||
}
|
||||
|
||||
/// Switch to the next image in the stream
|
||||
|
@ -747,7 +753,7 @@ class _ViewerState extends State<Viewer> {
|
|||
_pageController
|
||||
.nextPage(duration: k.animationDurationNormal, curve: Curves.easeInOut)
|
||||
.whenComplete(
|
||||
() => _updateNavigationState(_pageController.page.round()));
|
||||
() => _updateNavigationState(_pageController.page!.round()));
|
||||
}
|
||||
|
||||
/// Switch to the image on the "left", what that means depend on the current
|
||||
|
@ -819,7 +825,7 @@ class _ViewerState extends State<Viewer> {
|
|||
|
||||
var _isZoomed = false;
|
||||
|
||||
PageController _pageController;
|
||||
late PageController _pageController;
|
||||
final _pageStates = <int, _PageState>{};
|
||||
|
||||
/// used to gain focus on web for keyboard support
|
||||
|
@ -832,6 +838,6 @@ class _PageState {
|
|||
_PageState(this.scrollController);
|
||||
|
||||
ScrollController scrollController;
|
||||
double itemHeight;
|
||||
double? itemHeight;
|
||||
bool hasLoaded = false;
|
||||
}
|
||||
|
|
|
@ -32,9 +32,9 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
class ViewerDetailPane extends StatefulWidget {
|
||||
const ViewerDetailPane({
|
||||
Key key,
|
||||
@required this.account,
|
||||
@required this.file,
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.file,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -54,7 +54,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_log.info("[initState] Metadata missing in File");
|
||||
} else {
|
||||
_log.info("[initState] Metadata exists in File");
|
||||
if (widget.file.metadata.exif != null) {
|
||||
if (widget.file.metadata!.exif != null) {
|
||||
_initMetadata();
|
||||
}
|
||||
}
|
||||
|
@ -73,29 +73,29 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
const space = " ";
|
||||
if (widget.file.metadata?.imageWidth != null &&
|
||||
widget.file.metadata?.imageHeight != null) {
|
||||
final pixelCount =
|
||||
widget.file.metadata.imageWidth * widget.file.metadata.imageHeight;
|
||||
final pixelCount = widget.file.metadata!.imageWidth! *
|
||||
widget.file.metadata!.imageHeight!;
|
||||
if (pixelCount >= 500000) {
|
||||
final mpCount = pixelCount / 1000000.0;
|
||||
sizeSubStr += AppLocalizations.of(context)
|
||||
sizeSubStr += AppLocalizations.of(context)!
|
||||
.megapixelCount(mpCount.toStringAsFixed(1));
|
||||
sizeSubStr += space;
|
||||
}
|
||||
sizeSubStr += _byteSizeToString(widget.file.contentLength);
|
||||
sizeSubStr += _byteSizeToString(widget.file.contentLength ?? 0);
|
||||
}
|
||||
|
||||
String cameraSubStr = "";
|
||||
if (_fNumber != null) {
|
||||
cameraSubStr += "f/${_fNumber.toStringAsFixed(1)}$space";
|
||||
cameraSubStr += "f/${_fNumber!.toStringAsFixed(1)}$space";
|
||||
}
|
||||
if (_exposureTime != null) {
|
||||
cameraSubStr +=
|
||||
AppLocalizations.of(context).secondCountSymbol(_exposureTime);
|
||||
AppLocalizations.of(context)!.secondCountSymbol(_exposureTime!);
|
||||
cameraSubStr += space;
|
||||
}
|
||||
if (_focalLength != null) {
|
||||
cameraSubStr += AppLocalizations.of(context)
|
||||
.millimeterCountSymbol(_focalLength.toStringAsFixedTruncated(2));
|
||||
cameraSubStr += AppLocalizations.of(context)!
|
||||
.millimeterCountSymbol(_focalLength!.toStringAsFixedTruncated(2));
|
||||
cameraSubStr += space;
|
||||
}
|
||||
if (_isoSpeedRatings != null) {
|
||||
|
@ -112,12 +112,12 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
children: [
|
||||
_DetailPaneButton(
|
||||
icon: Icons.playlist_add_outlined,
|
||||
label: AppLocalizations.of(context).addToAlbumTooltip,
|
||||
label: AppLocalizations.of(context)!.addToAlbumTooltip,
|
||||
onPressed: () => _onAddToAlbumPressed(context),
|
||||
),
|
||||
_DetailPaneButton(
|
||||
icon: Icons.delete_outline,
|
||||
label: AppLocalizations.of(context).deleteTooltip,
|
||||
label: AppLocalizations.of(context)!.deleteTooltip,
|
||||
onPressed: () => _onDeletePressed(context),
|
||||
),
|
||||
],
|
||||
|
@ -154,7 +154,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
title: Text(
|
||||
"${widget.file.metadata.imageWidth} x ${widget.file.metadata.imageHeight}"),
|
||||
"${widget.file.metadata!.imageWidth} x ${widget.file.metadata!.imageHeight}"),
|
||||
subtitle: Text(sizeSubStr),
|
||||
)
|
||||
else
|
||||
|
@ -163,7 +163,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
Icons.aspect_ratio,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
title: Text(_byteSizeToString(widget.file.contentLength)),
|
||||
title: Text(_byteSizeToString(widget.file.contentLength ?? 0)),
|
||||
),
|
||||
if (_model != null)
|
||||
ListTile(
|
||||
|
@ -171,14 +171,14 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
Icons.camera_outlined,
|
||||
color: AppTheme.getSecondaryTextColor(context),
|
||||
),
|
||||
title: Text(_model),
|
||||
title: Text(_model!),
|
||||
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
|
||||
),
|
||||
if (features.isSupportMapView && _gps != null)
|
||||
SizedBox(
|
||||
height: 256,
|
||||
child: platform.Map(
|
||||
center: _gps,
|
||||
center: _gps!,
|
||||
zoom: 16,
|
||||
onTap: _onMapTap,
|
||||
),
|
||||
|
@ -190,35 +190,35 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
|
||||
/// Convert EXIF data to readable format
|
||||
void _initMetadata() {
|
||||
final exif = widget.file.metadata.exif;
|
||||
final exif = widget.file.metadata!.exif!;
|
||||
_log.info("[_initMetadata] $exif");
|
||||
|
||||
if (exif.make != null && exif.model != null) {
|
||||
_model = "${exif.make} ${exif.model}";
|
||||
}
|
||||
if (exif.fNumber != null) {
|
||||
_fNumber = exif.fNumber.toDouble();
|
||||
_fNumber = exif.fNumber!.toDouble();
|
||||
}
|
||||
if (exif.exposureTime != null) {
|
||||
if (exif.exposureTime.denominator == 1) {
|
||||
_exposureTime = exif.exposureTime.numerator.toString();
|
||||
if (exif.exposureTime!.denominator == 1) {
|
||||
_exposureTime = exif.exposureTime!.numerator.toString();
|
||||
} else {
|
||||
_exposureTime = exif.exposureTime.toString();
|
||||
}
|
||||
}
|
||||
if (exif.focalLength != null) {
|
||||
_focalLength = exif.focalLength.toDouble();
|
||||
_focalLength = exif.focalLength!.toDouble();
|
||||
}
|
||||
if (exif.isoSpeedRatings != null) {
|
||||
_isoSpeedRatings = exif.isoSpeedRatings;
|
||||
_isoSpeedRatings = exif.isoSpeedRatings!;
|
||||
}
|
||||
if (exif.gpsLatitudeRef != null &&
|
||||
exif.gpsLatitude != null &&
|
||||
exif.gpsLongitudeRef != null &&
|
||||
exif.gpsLongitude != null) {
|
||||
final lat = _gpsDmsToDouble(exif.gpsLatitude) *
|
||||
final lat = _gpsDmsToDouble(exif.gpsLatitude!) *
|
||||
(exif.gpsLatitudeRef == "S" ? -1 : 1);
|
||||
final lng = _gpsDmsToDouble(exif.gpsLongitude) *
|
||||
final lng = _gpsDmsToDouble(exif.gpsLongitude!) *
|
||||
(exif.gpsLongitudeRef == "W" ? -1 : 1);
|
||||
_log.fine("GPS: ($lat, $lng)");
|
||||
_gps = Tuple2(lat, lng);
|
||||
|
@ -238,7 +238,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_log.info("[_onAddToAlbumPressed] Album picked: ${value.name}");
|
||||
_addToAlbum(context, value).then((_) {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.addToAlbumSuccessNotification(value.name)),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -246,7 +246,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
} else {
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).addToAlbumFailureNotification),
|
||||
Text(AppLocalizations.of(context)!.addToAlbumFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
"[_onAddToAlbumPressed] Failed while showDialog", e, stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
"${AppLocalizations.of(context).addToAlbumFailureNotification}: "
|
||||
"${AppLocalizations.of(context)!.addToAlbumFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -265,10 +265,10 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
void _onDeletePressed(BuildContext context) async {
|
||||
_log.info("[_onDeletePressed] Removing file: ${widget.file.path}");
|
||||
var controller = SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context).deleteProcessingNotification),
|
||||
content: Text(AppLocalizations.of(context)!.deleteProcessingNotification),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
controller?.closed?.whenComplete(() {
|
||||
controller?.closed.whenComplete(() {
|
||||
controller = null;
|
||||
});
|
||||
try {
|
||||
|
@ -276,7 +276,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
AlbumRepo(AlbumCachedDataSource()))(widget.account, widget.file);
|
||||
controller?.close();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context).deleteSuccessNotification),
|
||||
content: Text(AppLocalizations.of(context)!.deleteSuccessNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
Navigator.of(context).pop();
|
||||
|
@ -289,7 +289,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
controller?.close();
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text("${AppLocalizations.of(context).deleteFailureNotification}: "
|
||||
Text("${AppLocalizations.of(context)!.deleteFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -300,7 +300,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
if (platform_k.isAndroid) {
|
||||
final intent = AndroidIntent(
|
||||
action: "action_view",
|
||||
data: Uri.encodeFull("geo:${_gps.item1},${_gps.item2}?z=16"),
|
||||
data: Uri.encodeFull("geo:${_gps!.item1},${_gps!.item2}?z=16"),
|
||||
);
|
||||
intent.launch();
|
||||
}
|
||||
|
@ -329,7 +329,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).updateDateTimeFailureNotification),
|
||||
AppLocalizations.of(context)!.updateDateTimeFailureNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
}
|
||||
|
@ -362,7 +362,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_log.info("[_addToAlbum] File already in album: ${widget.file.path}");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
"${AppLocalizations.of(context).addToAlbumAlreadyAddedNotification}"),
|
||||
"${AppLocalizations.of(context)!.addToAlbumAlreadyAddedNotification}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return Future.error(ArgumentError("File already in album"));
|
||||
|
@ -381,7 +381,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
_log.shout("[_addToAlbum] Failed while updating album", e, stacktrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
"${AppLocalizations.of(context).addToAlbumFailureNotification}: "
|
||||
"${AppLocalizations.of(context)!.addToAlbumFailureNotification}: "
|
||||
"${exception_util.toUserString(e, context)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
|
@ -389,22 +389,26 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
|||
}
|
||||
}
|
||||
|
||||
DateTime _dateTime;
|
||||
late DateTime _dateTime;
|
||||
// EXIF data
|
||||
String _model;
|
||||
double _fNumber;
|
||||
String _exposureTime;
|
||||
double _focalLength;
|
||||
int _isoSpeedRatings;
|
||||
Tuple2<double, double> _gps;
|
||||
String? _model;
|
||||
double? _fNumber;
|
||||
String? _exposureTime;
|
||||
double? _focalLength;
|
||||
int? _isoSpeedRatings;
|
||||
Tuple2<double, double>? _gps;
|
||||
|
||||
static final _log =
|
||||
Logger("widget.viewer_detail_pane._ViewerDetailPaneState");
|
||||
}
|
||||
|
||||
class _DetailPaneButton extends StatelessWidget {
|
||||
const _DetailPaneButton({Key key, this.icon, this.label, this.onPressed})
|
||||
: super(key: key);
|
||||
const _DetailPaneButton({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
|
@ -439,7 +443,7 @@ class _DetailPaneButton extends StatelessWidget {
|
|||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback? onPressed;
|
||||
}
|
||||
|
||||
String _byteSizeToString(int byteSize) {
|
||||
|
|
|
@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
version: 1.22.0+2200
|
||||
|
||||
environment:
|
||||
sdk: ">=2.10.0 <3.0.0"
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
|
|
@ -14,6 +14,7 @@ void main() {
|
|||
final json = <String, dynamic>{
|
||||
"version": Album.version,
|
||||
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||
"name": "",
|
||||
"provider": <String, dynamic>{
|
||||
"type": "static",
|
||||
"content": <String, dynamic>{
|
||||
|
@ -30,7 +31,8 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Album.fromJson(json),
|
||||
Album.fromJson(json,
|
||||
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||
Album(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
name: "",
|
||||
|
@ -63,7 +65,8 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Album.fromJson(json),
|
||||
Album.fromJson(json,
|
||||
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||
Album(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
name: "album",
|
||||
|
@ -114,7 +117,8 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Album.fromJson(json),
|
||||
Album.fromJson(json,
|
||||
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||
Album(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
name: "",
|
||||
|
@ -161,7 +165,8 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Album.fromJson(json),
|
||||
Album.fromJson(json,
|
||||
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||
Album(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
name: "",
|
||||
|
@ -203,7 +208,8 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Album.fromJson(json),
|
||||
Album.fromJson(json,
|
||||
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||
Album(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
name: "",
|
||||
|
@ -242,7 +248,8 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Album.fromJson(json),
|
||||
Album.fromJson(json,
|
||||
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||
Album(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
name: "",
|
||||
|
@ -260,6 +267,7 @@ void main() {
|
|||
final json = <String, dynamic>{
|
||||
"version": Album.version,
|
||||
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||
"name": "",
|
||||
"provider": <String, dynamic>{
|
||||
"type": "static",
|
||||
"content": <String, dynamic>{
|
||||
|
@ -279,7 +287,8 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Album.fromJson(json),
|
||||
Album.fromJson(json,
|
||||
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||
Album(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
name: "",
|
||||
|
|
|
@ -109,7 +109,7 @@ void main() {
|
|||
"version": Metadata.version,
|
||||
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||
};
|
||||
expect(Metadata.fromJson(json),
|
||||
expect(Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||
Metadata(lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901)));
|
||||
});
|
||||
|
||||
|
@ -120,7 +120,7 @@ void main() {
|
|||
"fileEtag": "8a3e0799b6f0711c23cc2d93950eceb5",
|
||||
};
|
||||
expect(
|
||||
Metadata.fromJson(json),
|
||||
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||
Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
|
||||
|
@ -134,7 +134,7 @@ void main() {
|
|||
"imageWidth": 1024,
|
||||
};
|
||||
expect(
|
||||
Metadata.fromJson(json),
|
||||
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||
Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
imageWidth: 1024,
|
||||
|
@ -148,7 +148,7 @@ void main() {
|
|||
"imageHeight": 768,
|
||||
};
|
||||
expect(
|
||||
Metadata.fromJson(json),
|
||||
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||
Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
imageHeight: 768,
|
||||
|
@ -164,7 +164,7 @@ void main() {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
Metadata.fromJson(json),
|
||||
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||
Metadata(
|
||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||
exif: Exif({
|
||||
|
|
Loading…
Reference in a new issue