Migrate to null-safety

This commit is contained in:
Ming Ming 2021-07-24 04:05:57 +08:00
parent 1a4779f465
commit df26b7a8c8
97 changed files with 1380 additions and 1315 deletions

View file

@ -18,11 +18,11 @@ class Account with EquatableMixin {
} }
Account copyWith({ Account copyWith({
String scheme, String? scheme,
String address, String? address,
String username, String? username,
String password, String? password,
List<String> roots, List<String>? roots,
}) { }) {
return Account( return Account(
scheme ?? this.scheme, scheme ?? this.scheme,
@ -39,7 +39,7 @@ class Account with EquatableMixin {
"scheme: '$scheme', " "scheme: '$scheme', "
"address: '$address', " "address: '$address', "
"username: '$username', " "username: '$username', "
"password: '${password?.isNotEmpty == true ? (kDebugMode ? password : '***') : null}', " "password: '${password.isNotEmpty == true ? (kDebugMode ? password : '***') : null}', "
"roots: List {'${roots.join('\', \'')}'}, " "roots: List {'${roots.join('\', \'')}'}, "
"}"; "}";
} }

View file

@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
@ -43,9 +42,9 @@ class Api {
Future<Response> request( Future<Response> request(
String method, String method,
String endpoint, { String endpoint, {
Map<String, String> header, Map<String, String>? header,
String body, String? body,
Uint8List bodyBytes, Uint8List? bodyBytes,
bool isResponseString = true, bool isResponseString = true,
}) async { }) async {
final url = _makeUri(endpoint); final url = _makeUri(endpoint);
@ -114,7 +113,7 @@ class _Files {
Api _api; Api _api;
Future<Response> delete({ Future<Response> delete({
@required String path, required String path,
}) async { }) async {
try { try {
return await _api.request("DELETE", path); return await _api.request("DELETE", path);
@ -125,7 +124,7 @@ class _Files {
} }
Future<Response> get({ Future<Response> get({
@required String path, required String path,
}) async { }) async {
try { try {
return await _api.request("GET", path, isResponseString: false); return await _api.request("GET", path, isResponseString: false);
@ -136,9 +135,9 @@ class _Files {
} }
Future<Response> put({ Future<Response> put({
@required String path, required String path,
String mime = "application/octet-stream", String mime = "application/octet-stream",
Uint8List content, required Uint8List content,
}) async { }) async {
try { try {
return await _api.request( return await _api.request(
@ -156,8 +155,8 @@ class _Files {
} }
Future<Response> propfind({ Future<Response> propfind({
@required String path, required String path,
int depth, int? depth,
getlastmodified, getlastmodified,
getetag, getetag,
getcontenttype, getcontenttype,
@ -176,8 +175,8 @@ class _Files {
hasPreview, hasPreview,
size, size,
richWorkspace, richWorkspace,
Map<String, String> customNamespaces, Map<String, String>? customNamespaces,
List<String> customProperties, List<String>? customProperties,
}) async { }) async {
try { try {
final bool hasDavNs = (getlastmodified != null || final bool hasDavNs = (getlastmodified != null ||
@ -287,10 +286,10 @@ class _Files {
/// [namespaces] should be specified in the format {"URI": "prefix"}, eg, /// [namespaces] should be specified in the format {"URI": "prefix"}, eg,
/// {"DAV:": "d"} /// {"DAV:": "d"}
Future<Response> proppatch({ Future<Response> proppatch({
@required String path, required String path,
Map<String, String> namespaces, Map<String, String>? namespaces,
Map<String, dynamic> set, Map<String, dynamic>? set,
List<String> remove, List<String>? remove,
}) async { }) async {
try { try {
final ns = <String, String>{ final ns = <String, String>{
@ -300,7 +299,7 @@ class _Files {
builder builder
..processing("xml", "version=\"1.0\"") ..processing("xml", "version=\"1.0\"")
..element("d:propertyupdate", namespaces: ns, nest: () { ..element("d:propertyupdate", namespaces: ns, nest: () {
if (set?.isNotEmpty == true) { if (set != null && set.isNotEmpty) {
builder.element("d:set", nest: () { builder.element("d:set", nest: () {
builder.element("d:prop", nest: () { builder.element("d:prop", nest: () {
for (final e in set.entries) { 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:remove", nest: () {
builder.element("d:prop", nest: () { builder.element("d:prop", nest: () {
for (final e in remove) { for (final e in remove) {
@ -331,7 +330,7 @@ class _Files {
/// A folder can be created by sending a MKCOL request to the folder /// A folder can be created by sending a MKCOL request to the folder
Future<Response> mkcol({ Future<Response> mkcol({
@required String path, required String path,
}) async { }) async {
try { try {
return await _api.request("MKCOL", path); 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 /// A file or folder can be copied by sending a COPY request to the file or
/// folder and specifying the [destinationUrl] as full url /// folder and specifying the [destinationUrl] as full url
Future<Response> copy({ Future<Response> copy({
@required String path, required String path,
@required String destinationUrl, required String destinationUrl,
bool overwrite, bool? overwrite,
}) async { }) async {
try { try {
return await _api.request("COPY", path, header: { 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 /// A file or folder can be moved by sending a MOVE request to the file or
/// folder and specifying the [destinationUrl] as full url /// folder and specifying the [destinationUrl] as full url
Future<Response> move({ Future<Response> move({
@required String path, required String path,
@required String destinationUrl, required String destinationUrl,
bool overwrite, bool? overwrite,
}) async { }) async {
try { try {
return await _api.request("MOVE", path, header: { return await _api.request("MOVE", path, header: {

View file

@ -1,5 +1,4 @@
/// Helper functions working with remote Nextcloud server /// Helper functions working with remote Nextcloud server
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart'; import 'package:nc_photos/api/api.dart';
@ -10,10 +9,10 @@ import 'package:nc_photos/exception.dart';
String getFilePreviewUrl( String getFilePreviewUrl(
Account account, Account account,
File file, { File file, {
@required int width, required int width,
@required int height, required int height,
String mode, String? mode,
bool a, bool? a,
}) { }) {
return "${account.url}/" return "${account.url}/"
"${getFilePreviewUrlRelative(file, width: width, height: height, mode: mode, a: a)}"; "${getFilePreviewUrlRelative(file, width: width, height: height, mode: mode, a: a)}";
@ -24,10 +23,10 @@ String getFilePreviewUrl(
/// cropped /// cropped
String getFilePreviewUrlRelative( String getFilePreviewUrlRelative(
File file, { File file, {
@required int width, required int width,
@required int height, required int height,
String mode, String? mode,
bool a, bool? a,
}) { }) {
String url; String url;
if (file.fileId != null) { if (file.fileId != null) {
@ -70,7 +69,7 @@ Future<String> exchangePassword(Account account) async {
try { try {
final appPwdRegex = RegExp(r"<apppassword>(.*)</apppassword>"); final appPwdRegex = RegExp(r"<apppassword>(.*)</apppassword>");
final appPwdMatch = appPwdRegex.firstMatch(response.body); final appPwdMatch = appPwdRegex.firstMatch(response.body);
return appPwdMatch.group(1); return appPwdMatch!.group(1)!;
} catch (_) { } catch (_) {
// this happens when the address is not the base URL and so Nextcloud // this happens when the address is not the base URL and so Nextcloud
// returned the login page // returned the login page

View file

@ -8,7 +8,6 @@ import 'package:nc_photos/entity/album/upgrader.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/mobile/platform.dart' import 'package:nc_photos/mobile/platform.dart'
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; 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'; import 'package:synchronized/synchronized.dart';
class AppDb { class AppDb {
@ -140,7 +139,7 @@ class AppDbAlbumEntry {
upgraderV1: AlbumUpgraderV1(), upgraderV1: AlbumUpgraderV1(),
upgraderV2: AlbumUpgraderV2(), upgraderV2: AlbumUpgraderV2(),
upgraderV3: AlbumUpgraderV3(), upgraderV3: AlbumUpgraderV3(),
), )!,
); );
} }

View file

@ -91,12 +91,12 @@ class AlbumSearchBloc extends Bloc<AlbumSearchBlocEvent, AlbumSearchBlocState> {
_albums = ev.albums; _albums = ev.albums;
if (_lastSearch != null) { if (_lastSearch != null) {
// search again // search again
yield* _onEventSearch(_lastSearch); yield* _onEventSearch(_lastSearch!);
} }
} }
var _albums = <Album>[]; var _albums = <Album>[];
AlbumSearchBlocSearchEvent _lastSearch; AlbumSearchBlocSearchEvent? _lastSearch;
static final _log = Logger("bloc.album_search.AlbumSearchBloc"); static final _log = Logger("bloc.album_search.AlbumSearchBloc");
} }

View file

@ -112,12 +112,12 @@ class AlbumSearchSuggestionBloc extends Bloc<AlbumSearchSuggestionBlocEvent,
} }
if (_lastSearch != null) { if (_lastSearch != null) {
// search again // search again
yield* _onEventSearch(_lastSearch); yield* _onEventSearch(_lastSearch!);
} }
} }
final _search = Woozy(limit: 5); final _search = Woozy(limit: 5);
AlbumSearchSuggestionBlocSearchEvent _lastSearch; AlbumSearchSuggestionBlocSearchEvent? _lastSearch;
static final _log = static final _log =
Logger("bloc.album_search_suggestion.AlbumSearchSuggestionBloc"); Logger("bloc.album_search_suggestion.AlbumSearchSuggestionBloc");

View file

@ -47,7 +47,7 @@ abstract class ListAlbumBlocState {
"}"; "}";
} }
final Account account; final Account? account;
final List<Album> albums; final List<Album> albums;
} }
@ -56,18 +56,18 @@ class ListAlbumBlocInit extends ListAlbumBlocState {
} }
class ListAlbumBlocLoading extends ListAlbumBlocState { class ListAlbumBlocLoading extends ListAlbumBlocState {
const ListAlbumBlocLoading(Account account, List<Album> albums) const ListAlbumBlocLoading(Account? account, List<Album> albums)
: super(account, albums); : super(account, albums);
} }
class ListAlbumBlocSuccess extends ListAlbumBlocState { class ListAlbumBlocSuccess extends ListAlbumBlocState {
const ListAlbumBlocSuccess(Account account, List<Album> albums) const ListAlbumBlocSuccess(Account? account, List<Album> albums)
: super(account, albums); : super(account, albums);
} }
class ListAlbumBlocFailure extends ListAlbumBlocState { class ListAlbumBlocFailure extends ListAlbumBlocState {
const ListAlbumBlocFailure( const ListAlbumBlocFailure(
Account account, List<Album> albums, this.exception) Account? account, List<Album> albums, this.exception)
: super(account, albums); : super(account, albums);
@override @override
@ -84,7 +84,7 @@ class ListAlbumBlocFailure extends ListAlbumBlocState {
/// The state of this bloc is inconsistent. This typically means that the data /// The state of this bloc is inconsistent. This typically means that the data
/// may have been changed externally /// may have been changed externally
class ListAlbumBlocInconsistent extends ListAlbumBlocState { class ListAlbumBlocInconsistent extends ListAlbumBlocState {
const ListAlbumBlocInconsistent(Account account, List<Album> albums) const ListAlbumBlocInconsistent(Account? account, List<Album> albums)
: super(account, albums); : super(account, albums);
} }
@ -134,7 +134,7 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
if (newState is ListAlbumBlocFailure) { if (newState is ListAlbumBlocFailure) {
yield ListAlbumBlocFailure( yield ListAlbumBlocFailure(
ev.account, ev.account,
newState.albums?.isNotEmpty == true ? newState.albums : state.albums, newState.albums.isNotEmpty ? newState.albums : state.albums,
newState.exception); newState.exception);
} else { } else {
yield newState; yield newState;
@ -206,9 +206,9 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
} }
} }
AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener; late AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
AppEventListener<FileRemovedEvent> _fileRemovedListener; late AppEventListener<FileRemovedEvent> _fileRemovedListener;
AppEventListener<AlbumCreatedEvent> _albumCreatedListener; late AppEventListener<AlbumCreatedEvent> _albumCreatedListener;
static final _log = Logger("bloc.list_album.ListAlbumBloc"); static final _log = Logger("bloc.list_album.ListAlbumBloc");
} }

View file

@ -13,9 +13,11 @@ class LsDirBlocItem {
if (isDeep) { if (isDeep) {
return "$runtimeType:${_toDeepString(0)}"; return "$runtimeType:${_toDeepString(0)}";
} else { } else {
final childrenStr =
children == null ? "null" : "List {length: ${children!.length}}";
return "$runtimeType {" return "$runtimeType {"
"file: '${file.path}', " "file: '${file.path}', "
"children: List {length: ${children.length}}, " "children: $childrenStr, "
"}"; "}";
} }
} }
@ -23,19 +25,19 @@ class LsDirBlocItem {
String _toDeepString(int level) { String _toDeepString(int level) {
String product = "\n" + " " * (level * 2) + "-${file.path}"; String product = "\n" + " " * (level * 2) + "-${file.path}";
if (children != null) { if (children != null) {
for (final c in children) { for (final c in children!) {
product += c._toDeepString(level + 1); product += c._toDeepString(level + 1);
} }
} }
return product; return product;
} }
File file; final File file;
/// Child directories under this directory /// Child directories under this directory
/// ///
/// Null if this dir is not listed, due to things like depth limitation /// Null if this dir is not listed, due to things like depth limitation
List<LsDirBlocItem> children; List<LsDirBlocItem>? children;
} }
abstract class LsDirBlocEvent { abstract class LsDirBlocEvent {
@ -59,9 +61,9 @@ class LsDirBlocQuery extends LsDirBlocEvent {
} }
LsDirBlocQuery copyWith({ LsDirBlocQuery copyWith({
Account account, Account? account,
File root, File? root,
int depth, int? depth,
}) { }) {
return LsDirBlocQuery( return LsDirBlocQuery(
account ?? this.account, account ?? this.account,
@ -76,11 +78,7 @@ class LsDirBlocQuery extends LsDirBlocEvent {
} }
abstract class LsDirBlocState { abstract class LsDirBlocState {
const LsDirBlocState(this._account, this._root, this._items); const LsDirBlocState(this.account, this.root, this.items);
Account get account => _account;
File get root => _root;
List<LsDirBlocItem> get items => _items;
@override @override
toString() { toString() {
@ -91,9 +89,9 @@ abstract class LsDirBlocState {
"}"; "}";
} }
final Account _account; final Account? account;
final File _root; final File root;
final List<LsDirBlocItem> _items; final List<LsDirBlocItem> items;
} }
class LsDirBlocInit extends LsDirBlocState { class LsDirBlocInit extends LsDirBlocState {
@ -101,18 +99,18 @@ class LsDirBlocInit extends LsDirBlocState {
} }
class LsDirBlocLoading 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); : super(account, root, items);
} }
class LsDirBlocSuccess extends LsDirBlocState { 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); : super(account, root, items);
} }
class LsDirBlocFailure extends LsDirBlocState { class LsDirBlocFailure extends LsDirBlocState {
const LsDirBlocFailure( const LsDirBlocFailure(
Account account, File root, List<LsDirBlocItem> items, this.exception) Account? account, File root, List<LsDirBlocItem> items, this.exception)
: super(account, root, items); : super(account, root, items);
@override @override
@ -153,12 +151,12 @@ class LsDirBloc extends Bloc<LsDirBlocEvent, LsDirBlocState> {
var files = _cache[ev.root.path]; var files = _cache[ev.root.path];
if (files == null) { if (files == null) {
files = (await Ls(FileRepo(FileWebdavDataSource()))(ev.account, ev.root)) files = (await Ls(FileRepo(FileWebdavDataSource()))(ev.account, ev.root))
.where((f) => f.isCollection) .where((f) => f.isCollection ?? false)
.toList(); .toList();
_cache[ev.root.path] = files; _cache[ev.root.path] = files;
} }
for (final f in files) { for (final f in files) {
List<LsDirBlocItem> children; List<LsDirBlocItem>? children;
if (ev.depth > 1) { if (ev.depth > 1) {
children = await _query(ev.copyWith(root: f, depth: ev.depth - 1)); children = await _query(ev.copyWith(root: f, depth: ev.depth - 1));
} }

View file

@ -58,10 +58,7 @@ class _ScanDirBlocExternalEvent extends ScanDirBlocEvent {
} }
abstract class ScanDirBlocState { abstract class ScanDirBlocState {
const ScanDirBlocState(this._account, this._files); const ScanDirBlocState(this.account, this.files);
Account get account => _account;
List<File> get files => _files;
@override @override
toString() { toString() {
@ -71,8 +68,8 @@ abstract class ScanDirBlocState {
"}"; "}";
} }
final Account _account; final Account? account;
final List<File> _files; final List<File> files;
} }
class ScanDirBlocInit extends ScanDirBlocState { class ScanDirBlocInit extends ScanDirBlocState {
@ -80,17 +77,17 @@ class ScanDirBlocInit extends ScanDirBlocState {
} }
class ScanDirBlocLoading extends ScanDirBlocState { class ScanDirBlocLoading extends ScanDirBlocState {
const ScanDirBlocLoading(Account account, List<File> files) const ScanDirBlocLoading(Account? account, List<File> files)
: super(account, files); : super(account, files);
} }
class ScanDirBlocSuccess extends ScanDirBlocState { class ScanDirBlocSuccess extends ScanDirBlocState {
const ScanDirBlocSuccess(Account account, List<File> files) const ScanDirBlocSuccess(Account? account, List<File> files)
: super(account, files); : super(account, files);
} }
class ScanDirBlocFailure extends ScanDirBlocState { 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); : super(account, files);
@override @override
@ -107,7 +104,7 @@ class ScanDirBlocFailure extends ScanDirBlocState {
/// The state of this bloc is inconsistent. This typically means that the data /// The state of this bloc is inconsistent. This typically means that the data
/// may have been changed externally /// may have been changed externally
class ScanDirBlocInconsistent extends ScanDirBlocState { class ScanDirBlocInconsistent extends ScanDirBlocState {
const ScanDirBlocInconsistent(Account account, List<File> files) const ScanDirBlocInconsistent(Account? account, List<File> files)
: super(account, files); : super(account, files);
} }
@ -285,11 +282,12 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
} }
} }
AppEventListener<FileRemovedEvent> _fileRemovedEventListener; late AppEventListener<FileRemovedEvent> _fileRemovedEventListener;
AppEventListener<FilePropertyUpdatedEvent> _filePropertyUpdatedEventListener; late AppEventListener<FilePropertyUpdatedEvent>
_filePropertyUpdatedEventListener;
int _successivePropertyUpdatedCount = 0; int _successivePropertyUpdatedCount = 0;
StreamSubscription<void> _propertyUpdatedSubscription; StreamSubscription<void>? _propertyUpdatedSubscription;
bool _shouldCheckCache = true; bool _shouldCheckCache = true;

View file

@ -7,12 +7,16 @@ class CancelableGetFile {
Future<FileInfo> getFileUntil(String key, Future<FileInfo> getFileUntil(String key,
{bool ignoreMemCache = false}) async { {bool ignoreMemCache = false}) async {
FileInfo product; FileInfo? product;
while (product == null && _shouldRun) { while (product == null && _shouldRun) {
product = await store.getFile(key, ignoreMemCache: ignoreMemCache); product = await store.getFile(key, ignoreMemCache: ignoreMemCache);
await Future.delayed(Duration(milliseconds: 500)); await Future.delayed(Duration(milliseconds: 500));
} }
return product ?? Future.error("Interrupted"); if (product == null) {
return Future.error("Interrupted");
} else {
return product;
}
} }
void cancel() { void cancel() {

View file

@ -2,7 +2,6 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:idb_sqflite/idb_sqflite.dart'; import 'package:idb_sqflite/idb_sqflite.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
@ -31,57 +30,57 @@ bool isAlbumFile(Account account, File file) =>
/// Immutable object that represents an album /// Immutable object that represents an album
class Album with EquatableMixin { class Album with EquatableMixin {
Album({ Album({
DateTime lastUpdated, DateTime? lastUpdated,
@required String name, required this.name,
@required this.provider, required this.provider,
@required this.coverProvider, required this.coverProvider,
@required this.sortProvider, required this.sortProvider,
this.albumFile, this.albumFile,
}) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc(), }) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc();
this.name = name ?? "";
factory Album.fromJson( static Album? fromJson(
Map<String, dynamic> json, { Map<String, dynamic> json, {
AlbumUpgraderV1 upgraderV1, required AlbumUpgraderV1? upgraderV1,
AlbumUpgraderV2 upgraderV2, required AlbumUpgraderV2? upgraderV2,
AlbumUpgraderV3 upgraderV3, required AlbumUpgraderV3? upgraderV3,
}) { }) {
final jsonVersion = json["version"]; final jsonVersion = json["version"];
Map<String, dynamic>? result = json;
if (jsonVersion < 2) { if (jsonVersion < 2) {
json = upgraderV1?.call(json); result = upgraderV1?.call(result);
if (json == null) { if (result == null) {
_log.info("[fromJson] Version $jsonVersion not compatible"); _log.info("[fromJson] Version $jsonVersion not compatible");
return null; return null;
} }
} }
if (jsonVersion < 3) { if (jsonVersion < 3) {
json = upgraderV2?.call(json); result = upgraderV2?.call(result);
if (json == null) { if (result == null) {
_log.info("[fromJson] Version $jsonVersion not compatible"); _log.info("[fromJson] Version $jsonVersion not compatible");
return null; return null;
} }
} }
if (jsonVersion < 4) { if (jsonVersion < 4) {
json = upgraderV3?.call(json); result = upgraderV3?.call(result);
if (json == null) { if (result == null) {
_log.info("[fromJson] Version $jsonVersion not compatible"); _log.info("[fromJson] Version $jsonVersion not compatible");
return null; return null;
} }
} }
return Album( return Album(
lastUpdated: json["lastUpdated"] == null lastUpdated: result["lastUpdated"] == null
? null ? null
: DateTime.parse(json["lastUpdated"]), : DateTime.parse(result["lastUpdated"]),
name: json["name"], name: result["name"],
provider: provider:
AlbumProvider.fromJson(json["provider"].cast<String, dynamic>()), AlbumProvider.fromJson(result["provider"].cast<String, dynamic>()),
coverProvider: AlbumCoverProvider.fromJson( coverProvider: AlbumCoverProvider.fromJson(
json["coverProvider"].cast<String, dynamic>()), result["coverProvider"].cast<String, dynamic>()),
sortProvider: AlbumSortProvider.fromJson( sortProvider: AlbumSortProvider.fromJson(
json["sortProvider"].cast<String, dynamic>()), result["sortProvider"].cast<String, dynamic>()),
albumFile: json["albumFile"] == null albumFile: result["albumFile"] == null
? 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 /// will be used. In order to keep [lastUpdated], you must explicitly assign
/// it with value from this or a null value /// it with value from this or a null value
Album copyWith({ Album copyWith({
OrNull<DateTime> lastUpdated, OrNull<DateTime>? lastUpdated,
String name, String? name,
AlbumProvider provider, AlbumProvider? provider,
AlbumCoverProvider coverProvider, AlbumCoverProvider? coverProvider,
AlbumSortProvider sortProvider, AlbumSortProvider? sortProvider,
File albumFile, File? albumFile,
}) { }) {
return Album( return Album(
lastUpdated: lastUpdated:
@ -141,7 +140,7 @@ class Album with EquatableMixin {
"provider": provider.toJson(), "provider": provider.toJson(),
"coverProvider": coverProvider.toJson(), "coverProvider": coverProvider.toJson(),
"sortProvider": sortProvider.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 /// How is this album stored on server
/// ///
/// This field is typically only meaningful when returned by [AlbumRepo.get] /// 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 /// versioning of this class, use to upgrade old persisted album
static const version = 4; static const version = 4;
@ -224,7 +223,8 @@ class AlbumRemoteDataSource implements AlbumDataSource {
upgraderV1: AlbumUpgraderV1(), upgraderV1: AlbumUpgraderV1(),
upgraderV2: AlbumUpgraderV2(), upgraderV2: AlbumUpgraderV2(),
upgraderV3: AlbumUpgraderV3(), upgraderV3: AlbumUpgraderV3(),
).copyWith( )!
.copyWith(
lastUpdated: OrNull(null), lastUpdated: OrNull(null),
albumFile: albumFile, albumFile: albumFile,
); );
@ -245,8 +245,8 @@ class AlbumRemoteDataSource implements AlbumDataSource {
final filePath = final filePath =
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName"; "${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
final fileRepo = FileRepo(FileWebdavDataSource()); final fileRepo = FileRepo(FileWebdavDataSource());
await PutFileBinary(fileRepo)( await PutFileBinary(fileRepo)(account, filePath,
account, filePath, utf8.encode(jsonEncode(album.toRemoteJson())), Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
shouldCreateMissingDir: true); shouldCreateMissingDir: true);
// query album file // query album file
final list = await Ls(fileRepo)(account, File(path: filePath), final list = await Ls(fileRepo)(account, File(path: filePath),
@ -256,10 +256,10 @@ class AlbumRemoteDataSource implements AlbumDataSource {
@override @override
update(Account account, Album album) async { update(Account account, Album album) async {
_log.info("[update] ${album.albumFile.path}"); _log.info("[update] ${album.albumFile!.path}");
final fileRepo = FileRepo(FileWebdavDataSource()); final fileRepo = FileRepo(FileWebdavDataSource());
await PutFileBinary(fileRepo)(account, album.albumFile.path, await PutFileBinary(fileRepo)(account, album.albumFile!.path,
utf8.encode(jsonEncode(album.toRemoteJson()))); Utf8Encoder().convert(jsonEncode(album.toRemoteJson())));
} }
@override @override
@ -286,7 +286,7 @@ class AlbumAppDbDataSource implements AlbumDataSource {
final path = AppDbAlbumEntry.toPathFromFile(account, albumFile); final path = AppDbAlbumEntry.toPathFromFile(account, albumFile);
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]); final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
final List results = await index.getAll(range); final List results = await index.getAll(range);
if (results?.isNotEmpty == true) { if (results.isNotEmpty == true) {
final entries = results final entries = results
.map((e) => AppDbAlbumEntry.fromJson(e.cast<String, dynamic>())); .map((e) => AppDbAlbumEntry.fromJson(e.cast<String, dynamic>()));
if (entries.length > 1) { if (entries.length > 1) {
@ -317,7 +317,7 @@ class AlbumAppDbDataSource implements AlbumDataSource {
@override @override
update(Account account, Album album) { update(Account account, Album album) {
_log.info("[update] ${album.albumFile.path}"); _log.info("[update] ${album.albumFile!.path}");
return AppDb.use((db) async { return AppDb.use((db) async {
final transaction = final transaction =
db.transaction(AppDb.albumStoreName, idbModeReadWrite); db.transaction(AppDb.albumStoreName, idbModeReadWrite);
@ -337,8 +337,8 @@ class AlbumCachedDataSource implements AlbumDataSource {
get(Account account, File albumFile) async { get(Account account, File albumFile) async {
try { try {
final cache = await _appDbSrc.get(account, albumFile); final cache = await _appDbSrc.get(account, albumFile);
if (cache.albumFile.etag?.isNotEmpty == true && if (cache.albumFile!.etag?.isNotEmpty == true &&
cache.albumFile.etag == albumFile.etag) { cache.albumFile!.etag == albumFile.etag) {
// cache is good // cache is good
_log.fine( _log.fine(
"[get] etag matched for ${AppDbAlbumEntry.toPathFromFile(account, albumFile)}"); "[get] etag matched for ${AppDbAlbumEntry.toPathFromFile(account, albumFile)}");
@ -412,7 +412,7 @@ class AlbumCachedDataSource implements AlbumDataSource {
Future<void> _cacheAlbum( Future<void> _cacheAlbum(
ObjectStore store, Account account, Album album) async { ObjectStore store, Account account, Album album) async {
final index = store.index(AppDbAlbumEntry.indexName); 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]); final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
// count number of entries for this album // count number of entries for this album
final count = await index.count(range); final count = await index.count(range);
@ -441,7 +441,7 @@ Future<void> _cacheAlbum(
for (final e in entries) { for (final e in entries) {
_log.info("[_cacheAlbum] Caching ${e.path}[${e.index}]"); _log.info("[_cacheAlbum] Caching ${e.path}[${e.index}]");
await store.put(e.toJson(), 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) { if (count > entries.length) {

View file

@ -40,7 +40,7 @@ abstract class AlbumCoverProvider with EquatableMixin {
@override @override
toString(); toString();
File getCover(Album album); File? getCover(Album album);
Map<String, dynamic> _toContentJson(); Map<String, dynamic> _toContentJson();
@ -68,7 +68,8 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
"}"; "}";
} }
File getCover(Album album) { @override
getCover(Album album) {
if (coverFile == null) { if (coverFile == null) {
try { try {
// use the latest file as cover // use the latest file as cover
@ -77,7 +78,8 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
.map((e) => e.file) .map((e) => e.file)
.where((element) => .where((element) =>
file_util.isSupportedFormat(element) && element.hasPreview) file_util.isSupportedFormat(element) &&
(element.hasPreview ?? false))
.sorted(compareFileDateTimeDescending) .sorted(compareFileDateTimeDescending)
.first; .first;
} catch (_) { } catch (_) {
@ -96,11 +98,11 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
@override @override
_toContentJson() { _toContentJson() {
return { return {
if (coverFile != null) "coverFile": coverFile.toJson(), if (coverFile != null) "coverFile": coverFile!.toJson(),
}; };
} }
final File coverFile; final File? coverFile;
static const _type = "auto"; static const _type = "auto";
} }

View file

@ -1,7 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/list_extension.dart'; import 'package:nc_photos/list_extension.dart';
@ -60,18 +59,16 @@ abstract class AlbumItem {
class AlbumFileItem extends AlbumItem with EquatableMixin { class AlbumFileItem extends AlbumItem with EquatableMixin {
AlbumFileItem({ AlbumFileItem({
@required this.file, required this.file,
}); });
@override @override
// ignore: hash_and_equals // 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) { if (other is AlbumFileItem) {
return super == other && return super == other && (file.equals(other.file, isDeep: isDeep));
(file == null) == (other.file == null) &&
(file?.equals(other.file, isDeep: isDeep) ?? true);
} else { } else {
return false; return false;
} }
@ -109,7 +106,7 @@ class AlbumFileItem extends AlbumItem with EquatableMixin {
class AlbumLabelItem extends AlbumItem with EquatableMixin { class AlbumLabelItem extends AlbumItem with EquatableMixin {
AlbumLabelItem({ AlbumLabelItem({
@required this.text, required this.text,
}); });
factory AlbumLabelItem.fromJson(Map<String, dynamic> json) { factory AlbumLabelItem.fromJson(Map<String, dynamic> json) {

View file

@ -50,7 +50,7 @@ abstract class AlbumProvider with EquatableMixin {
toString({bool isDeep = false}); toString({bool isDeep = false});
/// Return the date time associated with the latest item, or null /// Return the date time associated with the latest item, or null
DateTime get latestItemTime; DateTime? get latestItemTime;
AlbumProvider copyWith(); AlbumProvider copyWith();
@ -59,7 +59,7 @@ abstract class AlbumProvider with EquatableMixin {
class AlbumStaticProvider extends AlbumProvider { class AlbumStaticProvider extends AlbumProvider {
AlbumStaticProvider({ AlbumStaticProvider({
@required List<AlbumItem> items, required List<AlbumItem> items,
}) : this.items = UnmodifiableListView(items); }) : this.items = UnmodifiableListView(items);
factory AlbumStaticProvider.fromJson(Map<String, dynamic> json) { factory AlbumStaticProvider.fromJson(Map<String, dynamic> json) {
@ -90,7 +90,9 @@ class AlbumStaticProvider extends AlbumProvider {
} }
@override @override
AlbumStaticProvider copyWith({List<AlbumItem> items}) { AlbumStaticProvider copyWith({
List<AlbumItem>? items,
}) {
return AlbumStaticProvider( return AlbumStaticProvider(
items: items ?? this.items, items: items ?? this.items,
); );
@ -124,7 +126,7 @@ class AlbumStaticProvider extends AlbumProvider {
abstract class AlbumDynamicProvider extends AlbumProvider { abstract class AlbumDynamicProvider extends AlbumProvider {
AlbumDynamicProvider({ AlbumDynamicProvider({
DateTime latestItemTime, DateTime? latestItemTime,
}) : _latestItemTime = latestItemTime; }) : _latestItemTime = latestItemTime;
@override @override
@ -137,13 +139,13 @@ abstract class AlbumDynamicProvider extends AlbumProvider {
@override @override
toContentJson() { toContentJson() {
return { return {
"latestItemTime": _latestItemTime?.toUtc()?.toIso8601String(), "latestItemTime": _latestItemTime?.toUtc().toIso8601String(),
}; };
} }
@override @override
AlbumDynamicProvider copyWith({ AlbumDynamicProvider copyWith({
DateTime latestItemTime, DateTime? latestItemTime,
}); });
@override @override
@ -154,13 +156,13 @@ abstract class AlbumDynamicProvider extends AlbumProvider {
_latestItemTime, _latestItemTime,
]; ];
final DateTime _latestItemTime; final DateTime? _latestItemTime;
} }
class AlbumDirProvider extends AlbumDynamicProvider { class AlbumDirProvider extends AlbumDynamicProvider {
AlbumDirProvider({ AlbumDirProvider({
@required this.dirs, required this.dirs,
DateTime latestItemTime, DateTime? latestItemTime,
}) : super(latestItemTime: latestItemTime); }) : super(latestItemTime: latestItemTime);
factory AlbumDirProvider.fromJson(Map<String, dynamic> json) { factory AlbumDirProvider.fromJson(Map<String, dynamic> json) {
@ -192,8 +194,8 @@ class AlbumDirProvider extends AlbumDynamicProvider {
@override @override
AlbumDirProvider copyWith({ AlbumDirProvider copyWith({
List<File> dirs, List<File>? dirs,
DateTime latestItemTime, DateTime? latestItemTime,
}) { }) {
return AlbumDirProvider( return AlbumDirProvider(
dirs: dirs ?? this.dirs, dirs: dirs ?? this.dirs,

View file

@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/entity/album/item.dart'; import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
@ -80,7 +79,7 @@ class AlbumNullSortProvider extends AlbumSortProvider {
abstract class AlbumReversibleSortProvider extends AlbumSortProvider { abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
const AlbumReversibleSortProvider({ const AlbumReversibleSortProvider({
@required this.isAscending, required this.isAscending,
}); });
@override @override
@ -108,7 +107,7 @@ abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
/// Sort based on the time of the files /// Sort based on the time of the files
class AlbumTimeSortProvider extends AlbumReversibleSortProvider { class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
const AlbumTimeSortProvider({ const AlbumTimeSortProvider({
bool isAscending, required bool isAscending,
}) : super(isAscending: isAscending); }) : super(isAscending: isAscending);
factory AlbumTimeSortProvider.fromJson(Map<String, dynamic> json) { factory AlbumTimeSortProvider.fromJson(Map<String, dynamic> json) {
@ -126,7 +125,7 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
@override @override
sort(List<AlbumItem> items) { sort(List<AlbumItem> items) {
DateTime prevFileTime; DateTime? prevFileTime;
return items return items
.map((e) { .map((e) {
if (e is AlbumFileItem) { if (e is AlbumFileItem) {
@ -139,16 +138,16 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
.stableSorted((x, y) { .stableSorted((x, y) {
if (x.item1 == null && y.item1 == null) { if (x.item1 == null && y.item1 == null) {
return 0; return 0;
} } else if (x.item1 == null) {
else if (x.item1 == null) {
return -1; return -1;
} else if (y.item1 == null) { } else if (y.item1 == null) {
return 1; return 1;
}
if (isAscending) {
return x.item1.compareTo(y.item1);
} else { } 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) .map((e) => e.item2)

View file

@ -1,7 +1,7 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
abstract class AlbumUpgrader { abstract class AlbumUpgrader {
Map<String, dynamic> call(Map<String, dynamic> json); Map<String, dynamic>? call(Map<String, dynamic> json);
} }
/// Upgrade v1 Album to v2 /// Upgrade v1 Album to v2
@ -10,7 +10,8 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
this.logFilePath, 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 // v1 album items are corrupted in one of the updates, drop it
_log.fine("[call] Upgrade v1 Album for file: $logFilePath"); _log.fine("[call] Upgrade v1 Album for file: $logFilePath");
final result = Map<String, dynamic>.from(json); final result = Map<String, dynamic>.from(json);
@ -19,7 +20,7 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
} }
/// File path for logging only /// File path for logging only
final String logFilePath; final String? logFilePath;
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV1"); static final _log = Logger("entity.album.upgrader.AlbumUpgraderV1");
} }
@ -30,7 +31,8 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
this.logFilePath, this.logFilePath,
}); });
Map<String, dynamic> call(Map<String, dynamic> json) { @override
call(Map<String, dynamic> json) {
// move v2 items to v3 provider // move v2 items to v3 provider
_log.fine("[call] Upgrade v2 Album for file: $logFilePath"); _log.fine("[call] Upgrade v2 Album for file: $logFilePath");
final result = Map<String, dynamic>.from(json); final result = Map<String, dynamic>.from(json);
@ -51,7 +53,7 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
} }
/// File path for logging only /// File path for logging only
final String logFilePath; final String? logFilePath;
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV2"); static final _log = Logger("entity.album.upgrader.AlbumUpgraderV2");
} }
@ -62,7 +64,8 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
this.logFilePath, this.logFilePath,
}); });
Map<String, dynamic> call(Map<String, dynamic> json) { @override
call(Map<String, dynamic> json) {
// move v3 items to v4 provider // move v3 items to v4 provider
_log.fine("[call] Upgrade v3 Album for file: $logFilePath"); _log.fine("[call] Upgrade v3 Album for file: $logFilePath");
final result = Map<String, dynamic>.from(json); final result = Map<String, dynamic>.from(json);
@ -77,7 +80,7 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
} }
/// File path for logging only /// File path for logging only
final String logFilePath; final String? logFilePath;
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV3"); static final _log = Logger("entity.album.upgrader.AlbumUpgraderV3");
} }

View file

@ -9,14 +9,14 @@ class Exif with EquatableMixin {
@override @override
// ignore: hash_and_equals // ignore: hash_and_equals
bool operator ==(Object other) => equals(other, isDeep: true); bool operator ==(Object? other) => equals(other, isDeep: true);
/// Compare two Exif objects /// Compare two Exif objects
/// ///
/// If [isDeep] is false, two Exif objects are considered identical if they /// 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 /// contain the same number of fields. This hack is to save time comparing a
/// large amount of data that are mostly immutable /// large amount of data that are mostly immutable
bool equals(Object other, {bool isDeep = false}) { bool equals(Object? other, {bool isDeep = false}) {
if (isDeep) { if (isDeep) {
return super == other; return super == other;
} else { } else {
@ -87,33 +87,33 @@ class Exif with EquatableMixin {
} }
/// 0x010f Make /// 0x010f Make
String get make => data["Make"]; String? get make => data["Make"];
/// 0x0110 Model /// 0x0110 Model
String get model => data["Model"]; String? get model => data["Model"];
/// 0x9003 DateTimeOriginal /// 0x9003 DateTimeOriginal
DateTime get dateTimeOriginal => data.containsKey("DateTimeOriginal") DateTime? get dateTimeOriginal => data.containsKey("DateTimeOriginal")
? dateTimeFormat.parse(data["DateTimeOriginal"]).toUtc() ? dateTimeFormat.parse(data["DateTimeOriginal"]).toUtc()
: null; : null;
/// 0x829a ExposureTime /// 0x829a ExposureTime
Rational get exposureTime => data["ExposureTime"]; Rational? get exposureTime => data["ExposureTime"];
/// 0x829d FNumber /// 0x829d FNumber
Rational get fNumber => data["FNumber"]; Rational? get fNumber => data["FNumber"];
/// 0x8827 ISO/ISOSpeedRatings/PhotographicSensitivity /// 0x8827 ISO/ISOSpeedRatings/PhotographicSensitivity
int get isoSpeedRatings => data["ISOSpeedRatings"]; int? get isoSpeedRatings => data["ISOSpeedRatings"];
/// 0x920a FocalLength /// 0x920a FocalLength
Rational get focalLength => data["FocalLength"]; Rational? get focalLength => data["FocalLength"];
/// 0x8825 GPS tags /// 0x8825 GPS tags
String get gpsLatitudeRef => data["GPSLatitudeRef"]; String? get gpsLatitudeRef => data["GPSLatitudeRef"];
List<Rational> get gpsLatitude => data["GPSLatitude"].cast<Rational>(); List<Rational>? get gpsLatitude => data["GPSLatitude"].cast<Rational>();
String get gpsLongitudeRef => data["GPSLongitudeRef"]; String? get gpsLongitudeRef => data["GPSLongitudeRef"];
List<Rational> get gpsLongitude => data["GPSLongitude"].cast<Rational>(); List<Rational>? get gpsLongitude => data["GPSLongitude"].cast<Rational>();
@override @override
get props => [ get props => [

View file

@ -1,7 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/exif.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] /// Immutable object that hold metadata of a [File]
class Metadata with EquatableMixin { class Metadata with EquatableMixin {
Metadata({ Metadata({
DateTime lastUpdated, DateTime? lastUpdated,
this.fileEtag, this.fileEtag,
this.imageWidth, this.imageWidth,
this.imageHeight, this.imageHeight,
@ -30,9 +29,9 @@ class Metadata with EquatableMixin {
@override @override
// ignore: hash_and_equals // 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) { if (other is Metadata) {
return super == other && return super == other &&
(exif == null) == (other.exif == null) && (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, /// corresponding upgrader will be called one by one to upgrade the json,
/// version by version until it reached the active version. If any upgrader /// version by version until it reached the active version. If any upgrader
/// in the chain is null, the upgrade process will fail /// in the chain is null, the upgrade process will fail
factory Metadata.fromJson( static Metadata? fromJson(
Map<String, dynamic> json, { Map<String, dynamic> json, {
MetadataUpgraderV1 upgraderV1, required MetadataUpgraderV1? upgraderV1,
MetadataUpgraderV2 upgraderV2, required MetadataUpgraderV2? upgraderV2,
}) { }) {
final jsonVersion = json["version"]; final jsonVersion = json["version"];
Map<String, dynamic>? result = json;
if (jsonVersion < 2) { if (jsonVersion < 2) {
json = upgraderV1?.call(json); result = upgraderV1?.call(result);
if (json == null) { if (result == null) {
_log.info("[fromJson] Version $jsonVersion not compatible"); _log.info("[fromJson] Version $jsonVersion not compatible");
return null; return null;
} }
} }
if (jsonVersion < 3) { if (jsonVersion < 3) {
json = upgraderV2?.call(json); result = upgraderV2?.call(result);
if (json == null) { if (result == null) {
_log.info("[fromJson] Version $jsonVersion not compatible"); _log.info("[fromJson] Version $jsonVersion not compatible");
return null; return null;
} }
} }
return Metadata( return Metadata(
lastUpdated: json["lastUpdated"] == null lastUpdated: result["lastUpdated"] == null
? null ? null
: DateTime.parse(json["lastUpdated"]), : DateTime.parse(result["lastUpdated"]),
fileEtag: json["fileEtag"], fileEtag: result["fileEtag"],
imageWidth: json["imageWidth"], imageWidth: result["imageWidth"],
imageHeight: json["imageHeight"], imageHeight: result["imageHeight"],
exif: json["exif"] == null exif: result["exif"] == null
? 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 (fileEtag != null) "fileEtag": fileEtag,
if (imageWidth != null) "imageWidth": imageWidth, if (imageWidth != null) "imageWidth": imageWidth,
if (imageHeight != null) "imageHeight": imageHeight, 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; final DateTime lastUpdated;
/// Etag of the parent file when the metadata is saved /// Etag of the parent file when the metadata is saved
final String fileEtag; final String? fileEtag;
final int imageWidth; final int? imageWidth;
final int imageHeight; final int? imageHeight;
final Exif exif; final Exif? exif;
/// versioning of this class, use to upgrade old persisted metadata /// versioning of this class, use to upgrade old persisted metadata
static const version = 3; static const version = 3;
@ -135,17 +135,17 @@ class Metadata with EquatableMixin {
} }
abstract class MetadataUpgrader { abstract class MetadataUpgrader {
Map<String, dynamic> call(Map<String, dynamic> json); Map<String, dynamic>? call(Map<String, dynamic> json);
} }
/// Upgrade v1 Metadata to v2 /// Upgrade v1 Metadata to v2
class MetadataUpgraderV1 implements MetadataUpgrader { class MetadataUpgraderV1 implements MetadataUpgrader {
MetadataUpgraderV1({ MetadataUpgraderV1({
@required this.fileContentType, required this.fileContentType,
this.logFilePath, this.logFilePath,
}); });
Map<String, dynamic> call(Map<String, dynamic> json) { Map<String, dynamic>? call(Map<String, dynamic> json) {
if (fileContentType == "image/webp") { if (fileContentType == "image/webp") {
// Version 1 metadata for webp is bugged, drop it // Version 1 metadata for webp is bugged, drop it
_log.fine("[call] Upgrade v1 metadata for file: $logFilePath"); _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 /// File path for logging only
final String logFilePath; final String? logFilePath;
static final _log = Logger("entity.file.MetadataUpgraderV1"); static final _log = Logger("entity.file.MetadataUpgraderV1");
} }
@ -166,11 +166,11 @@ class MetadataUpgraderV1 implements MetadataUpgrader {
/// Upgrade v2 Metadata to v3 /// Upgrade v2 Metadata to v3
class MetadataUpgraderV2 implements MetadataUpgrader { class MetadataUpgraderV2 implements MetadataUpgrader {
MetadataUpgraderV2({ MetadataUpgraderV2({
@required this.fileContentType, required this.fileContentType,
this.logFilePath, this.logFilePath,
}); });
Map<String, dynamic> call(Map<String, dynamic> json) { Map<String, dynamic>? call(Map<String, dynamic> json) {
if (fileContentType == "image/jpeg") { if (fileContentType == "image/jpeg") {
// Version 2 metadata for jpeg doesn't consider orientation // Version 2 metadata for jpeg doesn't consider orientation
if (json["exif"] != null && json["exif"].containsKey("Orientation")) { if (json["exif"] != null && json["exif"].containsKey("Orientation")) {
@ -187,17 +187,17 @@ class MetadataUpgraderV2 implements MetadataUpgrader {
return json; return json;
} }
final String fileContentType; final String? fileContentType;
/// File path for logging only /// File path for logging only
final String logFilePath; final String? logFilePath;
static final _log = Logger("entity.file.MetadataUpgraderV2"); static final _log = Logger("entity.file.MetadataUpgraderV2");
} }
class File with EquatableMixin { class File with EquatableMixin {
File({ File({
@required String path, required String path,
this.contentLength, this.contentLength,
this.contentType, this.contentType,
this.etag, this.etag,
@ -214,9 +214,9 @@ class File with EquatableMixin {
@override @override
// ignore: hash_and_equals // 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) { if (other is File) {
return super == other && return super == other &&
(metadata == null) == (other.metadata == null) && (metadata == null) == (other.metadata == null) &&
@ -310,33 +310,33 @@ class File with EquatableMixin {
if (contentType != null) "contentType": contentType, if (contentType != null) "contentType": contentType,
if (etag != null) "etag": etag, if (etag != null) "etag": etag,
if (lastModified != null) if (lastModified != null)
"lastModified": lastModified.toUtc().toIso8601String(), "lastModified": lastModified!.toUtc().toIso8601String(),
if (isCollection != null) "isCollection": isCollection, if (isCollection != null) "isCollection": isCollection,
if (usedBytes != null) "usedBytes": usedBytes, if (usedBytes != null) "usedBytes": usedBytes,
if (hasPreview != null) "hasPreview": hasPreview, if (hasPreview != null) "hasPreview": hasPreview,
if (fileId != null) "fileId": fileId, if (fileId != null) "fileId": fileId,
if (ownerId != null) "ownerId": ownerId, if (ownerId != null) "ownerId": ownerId,
if (metadata != null) "metadata": metadata.toJson(), if (metadata != null) "metadata": metadata!.toJson(),
if (isArchived != null) "isArchived": isArchived, if (isArchived != null) "isArchived": isArchived,
if (overrideDateTime != null) if (overrideDateTime != null)
"overrideDateTime": overrideDateTime.toUtc().toIso8601String(), "overrideDateTime": overrideDateTime!.toUtc().toIso8601String(),
}; };
} }
File copyWith({ File copyWith({
String path, String? path,
int contentLength, int? contentLength,
String contentType, String? contentType,
String etag, String? etag,
DateTime lastModified, DateTime? lastModified,
bool isCollection, bool? isCollection,
int usedBytes, int? usedBytes,
bool hasPreview, bool? hasPreview,
int fileId, int? fileId,
String ownerId, String? ownerId,
OrNull<Metadata> metadata, OrNull<Metadata>? metadata,
OrNull<bool> isArchived, OrNull<bool>? isArchived,
OrNull<DateTime> overrideDateTime, OrNull<DateTime>? overrideDateTime,
}) { }) {
return File( return File(
path: path ?? this.path, path: path ?? this.path,
@ -392,20 +392,20 @@ class File with EquatableMixin {
]; ];
final String path; final String path;
final int contentLength; final int? contentLength;
final String contentType; final String? contentType;
final String etag; final String? etag;
final DateTime lastModified; final DateTime? lastModified;
final bool isCollection; final bool? isCollection;
final int usedBytes; final int? usedBytes;
final bool hasPreview; final bool? hasPreview;
// maybe null when loaded from old cache // maybe null when loaded from old cache
final int fileId; final int? fileId;
final String ownerId; final String? ownerId;
// metadata // metadata
final Metadata metadata; final Metadata? metadata;
final bool isArchived; final bool? isArchived;
final DateTime overrideDateTime; final DateTime? overrideDateTime;
} }
extension FileExtension on File { extension FileExtension on File {
@ -442,9 +442,9 @@ class FileRepo {
Future<void> updateProperty( Future<void> updateProperty(
Account account, Account account,
File file, { File file, {
OrNull<Metadata> metadata, OrNull<Metadata>? metadata,
OrNull<bool> isArchived, OrNull<bool>? isArchived,
OrNull<DateTime> overrideDateTime, OrNull<DateTime>? overrideDateTime,
}) => }) =>
this.dataSrc.updateProperty( this.dataSrc.updateProperty(
account, account,
@ -459,7 +459,7 @@ class FileRepo {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) => }) =>
this.dataSrc.copy( this.dataSrc.copy(
account, account,
@ -473,7 +473,7 @@ class FileRepo {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) => }) =>
this.dataSrc.move( this.dataSrc.move(
account, account,
@ -506,9 +506,9 @@ abstract class FileDataSource {
Future<void> updateProperty( Future<void> updateProperty(
Account account, Account account,
File f, { File f, {
OrNull<Metadata> metadata, OrNull<Metadata>? metadata,
OrNull<bool> isArchived, OrNull<bool>? isArchived,
OrNull<DateTime> overrideDateTime, OrNull<DateTime>? overrideDateTime,
}); });
/// Copy [f] to [destination] /// Copy [f] to [destination]
@ -519,7 +519,7 @@ abstract class FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}); });
/// Move [f] to [destination] /// Move [f] to [destination]
@ -530,7 +530,7 @@ abstract class FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}); });
/// Create a directory at [path] /// Create a directory at [path]

View file

@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:idb_shim/idb_client.dart'; import 'package:idb_shim/idb_client.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
@ -27,7 +26,7 @@ class FileWebdavDataSource implements FileDataSource {
list( list(
Account account, Account account,
File f, { File f, {
int depth, int? depth,
}) async { }) async {
_log.fine("[list] ${f.path}"); _log.fine("[list] ${f.path}");
final response = await Api(account).files().propfind( final response = await Api(account).files().propfind(
@ -61,7 +60,7 @@ class FileWebdavDataSource implements FileDataSource {
final files = WebdavFileParser()(xml); final files = WebdavFileParser()(xml);
// _log.fine("[list] Parsed files: [$files]"); // _log.fine("[list] Parsed files: [$files]");
return files.where((element) => _validateFile(element)).map((e) { 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; return e;
} else { } else {
_log.info("[list] Ignore outdated metadata for ${e.path}"); _log.info("[list] Ignore outdated metadata for ${e.path}");
@ -112,27 +111,27 @@ class FileWebdavDataSource implements FileDataSource {
updateProperty( updateProperty(
Account account, Account account,
File f, { File f, {
OrNull<Metadata> metadata, OrNull<Metadata>? metadata,
OrNull<bool> isArchived, OrNull<bool>? isArchived,
OrNull<DateTime> overrideDateTime, OrNull<DateTime>? overrideDateTime,
}) async { }) async {
_log.info("[updateProperty] ${f.path}"); _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( _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 = { final setProps = {
if (metadata?.obj != null) if (metadata?.obj != null)
"app:metadata": jsonEncode(metadata.obj.toJson()), "app:metadata": jsonEncode(metadata!.obj!.toJson()),
if (isArchived?.obj != null) "app:is-archived": isArchived.obj, if (isArchived?.obj != null) "app:is-archived": isArchived!.obj,
if (overrideDateTime?.obj != null) if (overrideDateTime?.obj != null)
"app:override-date-time": "app:override-date-time":
overrideDateTime.obj.toUtc().toIso8601String(), overrideDateTime!.obj!.toUtc().toIso8601String(),
}; };
final removeProps = [ final removeProps = [
if (OrNull.isNull(metadata)) "app:metadata", if (OrNull.isSetNull(metadata)) "app:metadata",
if (OrNull.isNull(isArchived)) "app:is-archived", if (OrNull.isSetNull(isArchived)) "app:is-archived",
if (OrNull.isNull(overrideDateTime)) "app:override-date-time", if (OrNull.isSetNull(overrideDateTime)) "app:override-date-time",
]; ];
final response = await Api(account).files().proppatch( final response = await Api(account).files().proppatch(
path: f.path, path: f.path,
@ -155,7 +154,7 @@ class FileWebdavDataSource implements FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) async { }) async {
_log.info("[copy] ${f.path} to $destination"); _log.info("[copy] ${f.path} to $destination");
final response = await Api(account).files().copy( final response = await Api(account).files().copy(
@ -176,7 +175,7 @@ class FileWebdavDataSource implements FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) async { }) async {
_log.info("[move] ${f.path} to $destination"); _log.info("[move] ${f.path} to $destination");
final response = await Api(account).files().move( final response = await Api(account).files().move(
@ -256,9 +255,9 @@ class FileAppDbDataSource implements FileDataSource {
updateProperty( updateProperty(
Account account, Account account,
File f, { File f, {
OrNull<Metadata> metadata, OrNull<Metadata>? metadata,
OrNull<bool> isArchived, OrNull<bool>? isArchived,
OrNull<DateTime> overrideDateTime, OrNull<DateTime>? overrideDateTime,
}) { }) {
_log.info("[updateProperty] ${f.path}"); _log.info("[updateProperty] ${f.path}");
return AppDb.use((db) async { return AppDb.use((db) async {
@ -300,7 +299,7 @@ class FileAppDbDataSource implements FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) async { }) async {
// do nothing // do nothing
} }
@ -310,7 +309,7 @@ class FileAppDbDataSource implements FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) async { }) async {
// do nothing // do nothing
} }
@ -325,7 +324,7 @@ class FileAppDbDataSource implements FileDataSource {
final path = AppDbFileEntry.toPath(account, f); final path = AppDbFileEntry.toPath(account, f);
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]); final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
final List results = await index.getAll(range); final List results = await index.getAll(range);
if (results?.isNotEmpty == true) { if (results.isNotEmpty == true) {
final entries = results final entries = results
.map((e) => AppDbFileEntry.fromJson(e.cast<String, dynamic>())); .map((e) => AppDbFileEntry.fromJson(e.cast<String, dynamic>()));
return entries return entries
@ -358,7 +357,7 @@ class FileCachedDataSource implements FileDataSource {
); );
final cache = await cacheManager.list(account, f); final cache = await cacheManager.list(account, f);
if (cacheManager.isGood) { if (cacheManager.isGood) {
return cache; return cache!;
} }
// no cache or outdated // no cache or outdated
@ -426,9 +425,9 @@ class FileCachedDataSource implements FileDataSource {
updateProperty( updateProperty(
Account account, Account account,
File f, { File f, {
OrNull<Metadata> metadata, OrNull<Metadata>? metadata,
OrNull<bool> isArchived, OrNull<bool>? isArchived,
OrNull<DateTime> overrideDateTime, OrNull<DateTime>? overrideDateTime,
}) async { }) async {
await _remoteSrc await _remoteSrc
.updateProperty( .updateProperty(
@ -462,7 +461,7 @@ class FileCachedDataSource implements FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) async { }) async {
await _remoteSrc.copy(account, f, destination, await _remoteSrc.copy(account, f, destination,
shouldOverwrite: shouldOverwrite); shouldOverwrite: shouldOverwrite);
@ -473,7 +472,7 @@ class FileCachedDataSource implements FileDataSource {
Account account, Account account,
File f, File f,
String destination, { String destination, {
bool shouldOverwrite, bool? shouldOverwrite,
}) async { }) async {
await _remoteSrc.move(account, f, destination, await _remoteSrc.move(account, f, destination,
shouldOverwrite: shouldOverwrite); shouldOverwrite: shouldOverwrite);
@ -599,17 +598,17 @@ class FileCachedDataSource implements FileDataSource {
class _CacheManager { class _CacheManager {
_CacheManager({ _CacheManager({
@required this.appDbSrc, required this.appDbSrc,
@required this.remoteSrc, required this.remoteSrc,
this.shouldCheckCache = false, this.shouldCheckCache = false,
}); });
/// Return the cached results of listing a directory [f] /// Return the cached results of listing a directory [f]
/// ///
/// Should check [isGood] before using the cache returning by this method /// 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("/"); final trimmedRootPath = f.path.trimAny("/");
List<File> cache; List<File>? cache;
try { try {
cache = await appDbSrc.list(account, f); cache = await appDbSrc.list(account, f);
// compare the cached root // compare the cached root
@ -645,7 +644,7 @@ class _CacheManager {
} }
bool get isGood => _isGood; bool get isGood => _isGood;
String get remoteTouchToken => _remoteToken; String? get remoteTouchToken => _remoteToken;
Future<void> _checkTouchToken( Future<void> _checkTouchToken(
Account account, File f, List<File> cache) async { Account account, File f, List<File> cache) async {
@ -653,7 +652,7 @@ class _CacheManager {
"${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}"; "${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}";
final fileRepo = FileRepo(FileCachedDataSource()); final fileRepo = FileRepo(FileCachedDataSource());
final tokenManager = TouchTokenManager(); final tokenManager = TouchTokenManager();
String remoteToken; String? remoteToken;
try { try {
remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f); remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f);
} catch (e, stacktrace) { } catch (e, stacktrace) {
@ -664,7 +663,7 @@ class _CacheManager {
} }
_remoteToken = remoteToken; _remoteToken = remoteToken;
String localToken; String? localToken;
try { try {
localToken = await tokenManager.getLocalToken(account, f); localToken = await tokenManager.getLocalToken(account, f);
} catch (e, stacktrace) { } catch (e, stacktrace) {
@ -687,7 +686,7 @@ class _CacheManager {
final bool shouldCheckCache; final bool shouldCheckCache;
var _isGood = false; var _isGood = false;
String _remoteToken; String? _remoteToken;
static final _log = Logger("entity.file.data_source._CacheManager"); static final _log = Logger("entity.file.data_source._CacheManager");
} }

View file

@ -31,7 +31,7 @@ class WebdavFileParser {
return null; return null;
} }
}) })
.where((element) => element != null) .whereType<File>()
.toList(); .toList();
} }
@ -55,19 +55,19 @@ class WebdavFileParser {
/// Map <DAV:response> contents to File /// Map <DAV:response> contents to File
File _toFile(XmlElement element) { File _toFile(XmlElement element) {
String path; String? path;
int contentLength; int? contentLength;
String contentType; String? contentType;
String etag; String? etag;
DateTime lastModified; DateTime? lastModified;
bool isCollection; bool? isCollection;
int usedBytes; int? usedBytes;
bool hasPreview; bool? hasPreview;
int fileId; int? fileId;
String ownerId; String? ownerId;
Metadata metadata; Metadata? metadata;
bool isArchived; bool? isArchived;
DateTime overrideDateTime; DateTime? overrideDateTime;
for (final child in element.children.whereType<XmlElement>()) { for (final child in element.children.whereType<XmlElement>()) {
if (child.matchQualifiedName("href", if (child.matchQualifiedName("href",
@ -105,7 +105,7 @@ class WebdavFileParser {
} }
return File( return File(
path: path, path: path!,
contentLength: contentLength, contentLength: contentLength,
contentType: contentType, contentType: contentType,
etag: etag, etag: etag,
@ -140,7 +140,10 @@ class WebdavFileParser {
} }
class _PropParser { class _PropParser {
_PropParser({this.namespaces = const {}, this.logFilePath}); _PropParser({
this.namespaces = const {},
this.logFilePath,
});
/// Parse <DAV:prop> element contents /// Parse <DAV:prop> element contents
void parse(XmlElement element) { void parse(XmlElement element) {
@ -201,43 +204,43 @@ class _PropParser {
} }
} }
DateTime get lastModified => _lastModified; DateTime? get lastModified => _lastModified;
int get contentLength => _contentLength; int? get contentLength => _contentLength;
String get contentType => _contentType; String? get contentType => _contentType;
String get etag => _etag; String? get etag => _etag;
int get usedBytes => _usedBytes; int? get usedBytes => _usedBytes;
bool get isCollection => _isCollection; bool? get isCollection => _isCollection;
bool get hasPreview => _hasPreview; bool? get hasPreview => _hasPreview;
int get fileId => _fileId; int? get fileId => _fileId;
String get ownerId => _ownerId; String? get ownerId => _ownerId;
Metadata get metadata => _metadata; Metadata? get metadata => _metadata;
bool get isArchived => _isArchived; bool? get isArchived => _isArchived;
DateTime get overrideDateTime => _overrideDateTime; DateTime? get overrideDateTime => _overrideDateTime;
final Map<String, String> namespaces; final Map<String, String> namespaces;
/// File path for logging only /// File path for logging only
final String logFilePath; final String? logFilePath;
DateTime _lastModified; DateTime? _lastModified;
int _contentLength; int? _contentLength;
String _contentType; String? _contentType;
String _etag; String? _etag;
int _usedBytes; int? _usedBytes;
bool _isCollection; bool? _isCollection;
bool _hasPreview; bool? _hasPreview;
int _fileId; int? _fileId;
String _ownerId; String? _ownerId;
Metadata _metadata; Metadata? _metadata;
bool _isArchived; bool? _isArchived;
DateTime _overrideDateTime; DateTime? _overrideDateTime;
} }
extension on XmlElement { extension on XmlElement {
bool matchQualifiedName( bool matchQualifiedName(
String local, { String local, {
String prefix, required String prefix,
Map<String, String> namespaces, required Map<String, String> namespaces,
}) { }) {
final localNamespaces = <String, String>{}; final localNamespaces = <String, String>{};
for (final a in attributes) { for (final a in attributes) {

View file

@ -23,13 +23,13 @@ class AppEventListener<T> {
_log.warning("[endListenEvent] Already not listening"); _log.warning("[endListenEvent] Already not listening");
return; return;
} }
_subscription.cancel(); _subscription?.cancel();
_subscription = null; _subscription = null;
} }
final void Function(T) _listener; final void Function(T) _listener;
final _stream = KiwiContainer().resolve<EventBus>().on<T>(); final _stream = KiwiContainer().resolve<EventBus>().on<T>();
StreamSubscription<T> _subscription; StreamSubscription<T>? _subscription;
final _log = Logger("event.event.AppEventListener<${T.runtimeType}>"); final _log = Logger("event.event.AppEventListener<${T.runtimeType}>");
} }

View file

@ -16,7 +16,10 @@ class CacheNotFoundException implements Exception {
} }
class ApiException implements Exception { class ApiException implements Exception {
ApiException({this.response, this.message}); ApiException({
required this.response,
this.message,
});
@override @override
toString() { toString() {

View file

@ -10,16 +10,16 @@ import 'package:nc_photos/exception.dart';
String toUserString(dynamic exception, BuildContext context) { String toUserString(dynamic exception, BuildContext context) {
if (exception is ApiException) { if (exception is ApiException) {
if (exception.response.statusCode == 401) { if (exception.response.statusCode == 401) {
return AppLocalizations.of(context).errorUnauthenticated; return AppLocalizations.of(context)!.errorUnauthenticated;
} else if (exception.response.statusCode == 423) { } else if (exception.response.statusCode == 423) {
return AppLocalizations.of(context).errorLocked; return AppLocalizations.of(context)!.errorLocked;
} else if (exception.response.statusCode == 500) { } else if (exception.response.statusCode == 500) {
return AppLocalizations.of(context).errorServerError; return AppLocalizations.of(context)!.errorServerError;
} }
} else if (exception is SocketException) { } else if (exception is SocketException) {
return AppLocalizations.of(context).errorDisconnected; return AppLocalizations.of(context)!.errorDisconnected;
} else if (exception is InvalidBaseUrlException) { } else if (exception is InvalidBaseUrlException) {
return AppLocalizations.of(context).errorInvalidBaseUrl; return AppLocalizations.of(context)!.errorInvalidBaseUrl;
} }
return exception.toString(); return exception.toString();
} }

View file

@ -3,10 +3,10 @@ import 'package:tuple/tuple.dart';
extension IterableExtension<T> on Iterable<T> { extension IterableExtension<T> on Iterable<T> {
/// Return a new sorted list /// 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 /// 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(); final tmp = this.toList();
mergeSort(tmp, compare: compare); mergeSort(tmp, compare: compare);
return tmp; return tmp;

View file

@ -6,12 +6,12 @@ class AppLanguage {
final int langId; final int langId;
final String nativeName; final String nativeName;
final Locale locale; final Locale? locale;
} }
String getSelectedLanguageName(BuildContext context) => String getSelectedLanguageName(BuildContext context) =>
_getSelectedLanguage(context).nativeName; _getSelectedLanguage(context).nativeName;
Locale getSelectedLocale(BuildContext context) => Locale? getSelectedLocale(BuildContext context) =>
_getSelectedLanguage(context).locale; _getSelectedLanguage(context).locale;
final supportedLanguages = { final supportedLanguages = {
@ -32,9 +32,9 @@ enum _AppLanguageEnum {
AppLanguage _getSelectedLanguage(BuildContext context) { AppLanguage _getSelectedLanguage(BuildContext context) {
try { try {
final lang = Pref.inst().getLanguage(); final lang = Pref.inst().getLanguageOr(0);
return supportedLanguages[lang]; return supportedLanguages[lang]!;
} catch (_) { } catch (_) {
return supportedLanguages[_AppLanguageEnum.systemDefault.index]; return supportedLanguages[_AppLanguageEnum.systemDefault.index]!;
} }
} }

View file

@ -72,8 +72,8 @@ void _initLog() {
Future<void> _initPref() async { Future<void> _initPref() async {
await Pref.init(); await Pref.init();
if (Pref.inst().getLastVersion(null) == null) { if (Pref.inst().getLastVersion() == null) {
if (Pref.inst().getSetupProgress(null) == null) { if (Pref.inst().getSetupProgress() == null) {
// new install // new install
await Pref.inst().setLastVersion(k.version); await Pref.inst().setLastVersion(k.version);
} else { } else {

View file

@ -25,7 +25,7 @@ class MetadataTask {
final op = UpdateMissingMetadata(fileRepo); final op = UpdateMissingMetadata(fileRepo);
await for (final _ in op(account, await for (final _ in op(account,
File(path: "${api_util.getWebdavRootUrlRelative(account)}/$r"))) { File(path: "${api_util.getWebdavRootUrlRelative(account)}/$r"))) {
if (!Pref.inst().isEnableExif()) { if (!Pref.inst().isEnableExifOr()) {
_log.info("[call] EXIF disabled, task ending immaturely"); _log.info("[call] EXIF disabled, task ending immaturely");
op.stop(); op.stop();
return; return;
@ -53,7 +53,7 @@ class MetadataTaskManager {
void _handleStream() async { void _handleStream() async {
await for (final task in _streamController.stream) { await for (final task in _streamController.stream) {
if (Pref.inst().isEnableExif()) { if (Pref.inst().isEnableExifOr()) {
_log.info("[_doTask] Executing task: $task"); _log.info("[_doTask] Executing task: $task");
await task(); await task();
} else { } else {

View file

@ -6,11 +6,13 @@ class MediaStore {
static const exceptionCodePermissionError = "permissionError"; static const exceptionCodePermissionError = "permissionError";
static Future<String> saveFileToDownload( static Future<String> saveFileToDownload(
String fileName, Uint8List fileContent) => String fileName, Uint8List fileContent) async {
_channel.invokeMethod("saveFileToDownload", <String, dynamic>{ return (await _channel
.invokeMethod<String>("saveFileToDownload", <String, dynamic>{
"fileName": fileName, "fileName": fileName,
"content": fileContent, "content": fileContent,
}); }))!;
}
static const _channel = static const _channel =
const MethodChannel("com.nkming.nc_photos/media_store"); const MethodChannel("com.nkming.nc_photos/media_store");

View file

@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
class Notification { class Notification {
static Future<void> notifyItemsDownloadSuccessful( static Future<void> notifyItemsDownloadSuccessful(
List<String> fileUris, List<String> mimeTypes) => List<String> fileUris, List<String?> mimeTypes) =>
_channel.invokeMethod("notifyItemsDownloadSuccessful", <String, dynamic>{ _channel.invokeMethod("notifyItemsDownloadSuccessful", <String, dynamic>{
"fileUris": fileUris, "fileUris": fileUris,
"mimeTypes": mimeTypes, "mimeTypes": mimeTypes,

View file

@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
class Share { class Share {
static Future<void> shareItems( static Future<void> shareItems(
List<String> fileUris, List<String> mimeTypes) => List<String> fileUris, List<String?> mimeTypes) =>
_channel.invokeMethod("shareItems", <String, dynamic>{ _channel.invokeMethod("shareItems", <String, dynamic>{
"fileUris": fileUris, "fileUris": fileUris,
"mimeTypes": mimeTypes, "mimeTypes": mimeTypes,

View file

@ -4,9 +4,9 @@ import 'package:tuple/tuple.dart';
class Map extends StatelessWidget { class Map extends StatelessWidget {
const Map({ const Map({
Key key, Key? key,
this.center, required this.center,
this.zoom, required this.zoom,
this.onTap, this.onTap,
}) : super(key: key); }) : super(key: key);
@ -44,5 +44,5 @@ class Map extends StatelessWidget {
/// A pair of latitude and longitude coordinates, stored as degrees /// A pair of latitude and longitude coordinates, stored as degrees
final Tuple2<double, double> center; final Tuple2<double, double> center;
final double zoom; final double zoom;
final void Function() onTap; final VoidCallback? onTap;
} }

View file

@ -11,5 +11,5 @@ class AndroidItemDownloadSuccessfulNotification
} }
final List<String> fileUris; final List<String> fileUris;
final List<String> mimeTypes; final List<String?> mimeTypes;
} }

View file

@ -103,7 +103,7 @@ class SelfSignedCertManager {
} }
} }
_BadCertInfo _latestBadCert; late _BadCertInfo _latestBadCert;
var _whitelist = <_CertInfo>[]; var _whitelist = <_CertInfo>[];
static SelfSignedCertManager _inst = SelfSignedCertManager._(); static SelfSignedCertManager _inst = SelfSignedCertManager._();
@ -168,7 +168,7 @@ class _BadCertInfo {
class _CustomHttpOverrides extends HttpOverrides { class _CustomHttpOverrides extends HttpOverrides {
@override @override
HttpClient createHttpClient(SecurityContext context) { HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context) return super.createHttpClient(context)
..badCertificateCallback = (cert, host, port) { ..badCertificateCallback = (cert, host, port) {
try { try {

View file

@ -10,5 +10,5 @@ class AndroidShare extends itf.Share {
} }
final List<String> fileUris; final List<String> fileUris;
final List<String> mimeTypes; final List<String?> mimeTypes;
} }

View file

@ -2,7 +2,9 @@
class OrNull<T> { class OrNull<T> {
OrNull(this.obj); 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;
} }

View file

@ -9,13 +9,13 @@ abstract class UniversalStorage {
/// Return the content associated with [name], or null if no such association /// Return the content associated with [name], or null if no such association
/// exists /// exists
Future<Uint8List> getBinary(String name); Future<Uint8List?> getBinary(String name);
Future<void> putString(String name, String content); Future<void> putString(String name, String content);
/// Return the string associated with [name], or null if no such association /// Return the string associated with [name], or null if no such association
/// exists /// exists
Future<String> getString(String name); Future<String?> getString(String name);
Future<void> remove(String name); Future<void> remove(String name);
} }

View file

@ -12,67 +12,65 @@ class Pref {
factory Pref.inst() => _inst; factory Pref.inst() => _inst;
List<Account> getAccounts([List<Account> def]) { List<Account>? getAccounts() {
final jsonObjs = _pref.getStringList("accounts"); final jsonObjs = _pref.getStringList("accounts");
return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e)))?.toList() ?? return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e))).toList();
def;
} }
List<Account> getAccountsOr(List<Account> def) => getAccounts() ?? def;
Future<bool> setAccounts(List<Account> value) { Future<bool> setAccounts(List<Account> value) {
final jsons = value.map((e) => jsonEncode(e.toJson())).toList(); final jsons = value.map((e) => jsonEncode(e.toJson())).toList();
return _pref.setStringList("accounts", jsons); return _pref.setStringList("accounts", jsons);
} }
int getCurrentAccountIndex([int def]) => int? getCurrentAccountIndex() => _pref.getInt("currentAccountIndex");
_pref.getInt("currentAccountIndex") ?? def; int getCurrentAccountIndexOr(int def) => getCurrentAccountIndex() ?? def;
Future<bool> setCurrentAccountIndex(int value) => Future<bool> setCurrentAccountIndex(int value) =>
_pref.setInt("currentAccountIndex", value); _pref.setInt("currentAccountIndex", value);
int getHomePhotosZoomLevel([int def]) => int? getHomePhotosZoomLevel() => _pref.getInt("homePhotosZoomLevel");
_pref.getInt("homePhotosZoomLevel") ?? def; int getHomePhotosZoomLevelOr(int def) => getHomePhotosZoomLevel() ?? def;
Future<bool> setHomePhotosZoomLevel(int value) => Future<bool> setHomePhotosZoomLevel(int value) =>
_pref.setInt("homePhotosZoomLevel", value); _pref.setInt("homePhotosZoomLevel", value);
int getAlbumViewerZoomLevel([int def]) => int? getAlbumViewerZoomLevel() => _pref.getInt("albumViewerZoomLevel");
_pref.getInt("albumViewerZoomLevel") ?? def; int getAlbumViewerZoomLevelOr(int def) => getAlbumViewerZoomLevel() ?? def;
Future<bool> setAlbumViewerZoomLevel(int value) => Future<bool> setAlbumViewerZoomLevel(int value) =>
_pref.setInt("albumViewerZoomLevel", 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) => Future<bool> setEnableExif(bool value) =>
_pref.setBool("isEnableExif", 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) => Future<bool> setSetupProgress(int value) =>
_pref.setInt("setupProgress", value); _pref.setInt("setupProgress", value);
/// Return the version number when the app last ran /// 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); 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); 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); Future<bool> setLanguage(int value) => _pref.setInt("language", value);
Pref._(); Pref._();
static final _inst = Pref._(); static final _inst = Pref._();
SharedPreferences _pref; late SharedPreferences _pref;
} }
extension PrefExtension on Pref { extension PrefExtension on Pref {
Account getCurrentAccount() { Account? getCurrentAccount() {
try { try {
return Pref.inst().getAccounts()[Pref.inst().getCurrentAccountIndex()]; return Pref.inst().getAccounts()![Pref.inst().getCurrentAccountIndex()!];
} catch (_) { } catch (_) {
return null; return null;
} }

View file

@ -23,7 +23,7 @@ class ShareHandler {
showDialog( showDialog(
context: context, context: context,
builder: (context) => ProcessingDialog( builder: (context) => ProcessingDialog(
text: AppLocalizations.of(context).shareDownloadingDialogContent), text: AppLocalizations.of(context)!.shareDownloadingDialogContent),
); );
final results = <Tuple2<File, dynamic>>[]; final results = <Tuple2<File, dynamic>>[];
for (final f in files) { for (final f in files) {
@ -33,7 +33,7 @@ class ShareHandler {
} on PermissionException catch (_) { } on PermissionException catch (_) {
_log.warning("[shareFiles] Permission not granted"); _log.warning("[shareFiles] Permission not granted");
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.downloadFailureNoPermissionNotification), .downloadFailureNoPermissionNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));

View file

@ -22,7 +22,7 @@ class SnackBarManager {
/// Show a snack bar if possible /// Show a snack bar if possible
/// ///
/// If the snack bar can't be shown at this time, return null /// If the snack bar can't be shown at this time, return null
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar( ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
SnackBar snackBar) { SnackBar snackBar) {
for (final h in _handlers.reversed) { for (final h in _handlers.reversed) {
final result = h.showSnackBar(snackBar); final result = h.showSnackBar(snackBar);
@ -42,6 +42,6 @@ class SnackBarManager {
} }
abstract class SnackBarHandler { abstract class SnackBarHandler {
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar( ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
SnackBar snackBar); SnackBar snackBar);
} }

View file

@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class AppTheme extends StatelessWidget { class AppTheme extends StatelessWidget {
const AppTheme({@required this.child}); const AppTheme({
required this.child,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -46,24 +48,24 @@ class AppTheme extends StatelessWidget {
static Color getSelectionOverlayColor(BuildContext context) { static Color getSelectionOverlayColor(BuildContext context) {
return Theme.of(context).brightness == Brightness.light return Theme.of(context).brightness == Brightness.light
? primarySwatchLight[100].withOpacity(0.7) ? primarySwatchLight[100]!.withOpacity(0.7)
: primarySwatchDark[700].withOpacity(0.7); : primarySwatchDark[700]!.withOpacity(0.7);
} }
static Color getSelectionCheckColor(BuildContext context) { static Color getSelectionCheckColor(BuildContext context) {
return Theme.of(context).brightness == Brightness.light return Theme.of(context).brightness == Brightness.light
? Colors.grey[800] ? Colors.grey[800]!
: Colors.grey[350]; : Colors.grey[350]!;
} }
static Color getOverscrollIndicatorColor(BuildContext context) { static Color getOverscrollIndicatorColor(BuildContext context) {
return Theme.of(context).brightness == Brightness.light return Theme.of(context).brightness == Brightness.light
? Colors.grey[800] ? Colors.grey[800]!
: Colors.grey[200]; : Colors.grey[200]!;
} }
static Color getRootPickerContentBoxColor(BuildContext context) { static Color getRootPickerContentBoxColor(BuildContext context) {
return Colors.blue[200]; return Colors.blue[200]!;
} }
static Color getPrimaryTextColor(BuildContext context) { static Color getPrimaryTextColor(BuildContext context) {

View file

@ -21,7 +21,7 @@ import 'package:nc_photos/use_case/remove.dart';
/// doing it on every query /// doing it on every query
class TouchTokenManager { class TouchTokenManager {
Future<void> setRemoteToken( Future<void> setRemoteToken(
FileRepo fileRepo, Account account, File file, String token) async { FileRepo fileRepo, Account account, File file, String? token) async {
_log.info( _log.info(
"[setRemoteToken] Set remote token for file '${file.path}': $token"); "[setRemoteToken] Set remote token for file '${file.path}': $token");
final path = _getRemotePath(account, file); 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 /// Return the touch token for [file] from remote source, or null if no such
/// file /// file
Future<String> getRemoteToken( Future<String?> getRemoteToken(
FileRepo fileRepo, Account account, File file) async { FileRepo fileRepo, Account account, File file) async {
final path = _getRemotePath(account, file); final path = _getRemotePath(account, file);
try { 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( _log.info(
"[setLocalToken] Set local token for file '${file.path}': $token"); "[setLocalToken] Set local token for file '${file.path}': $token");
final name = _getLocalStorageName(account, file); 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); final name = _getLocalStorageName(account, file);
return platform.UniversalStorage().getString(name); return platform.UniversalStorage().getString(name);
} }

View file

@ -22,9 +22,9 @@ class LoadMetadata {
} }
Future<Map<String, dynamic>> _loadMetadata({ Future<Map<String, dynamic>> _loadMetadata({
@required File file, required File file,
exifdart.AbstractBlobReader Function() exifdartReaderBuilder, required exifdart.AbstractBlobReader Function() exifdartReaderBuilder,
AsyncImageInput Function() imageSizeGetterInputBuilder, required AsyncImageInput Function() imageSizeGetterInputBuilder,
}) async { }) async {
var metadata = exifdart.Metadata(); var metadata = exifdart.Metadata();
if (file_util.isMetadataSupportedFormat(file)) { if (file_util.isMetadataSupportedFormat(file)) {
@ -47,8 +47,8 @@ class LoadMetadata {
await AsyncImageSizeGetter.getSize(imageSizeGetterInputBuilder()); await AsyncImageSizeGetter.getSize(imageSizeGetterInputBuilder());
// image size getter doesn't handle exif orientation // image size getter doesn't handle exif orientation
if (metadata.exif?.containsKey("Orientation") == true && if (metadata.exif?.containsKey("Orientation") == true &&
metadata.exif["Orientation"] >= 5 && metadata.exif!["Orientation"] >= 5 &&
metadata.exif["Orientation"] <= 8) { metadata.exif!["Orientation"] <= 8) {
// 90 deg CW/CCW // 90 deg CW/CCW
imageWidth = resolution.height; imageWidth = resolution.height;
imageHeight = resolution.width; imageHeight = resolution.width;
@ -66,12 +66,12 @@ class LoadMetadata {
} }
} else { } else {
if (metadata.rotateAngleCcw != null && if (metadata.rotateAngleCcw != null &&
metadata.rotateAngleCcw % 180 != 0) { metadata.rotateAngleCcw! % 180 != 0) {
imageWidth = metadata.imageHeight; imageWidth = metadata.imageHeight!;
imageHeight = metadata.imageWidth; imageHeight = metadata.imageWidth!;
} else { } else {
imageWidth = metadata.imageWidth; imageWidth = metadata.imageWidth!;
imageHeight = metadata.imageHeight; imageHeight = metadata.imageHeight!;
} }
} }

View file

@ -24,6 +24,7 @@ class Remove {
} }
Future<void> _cleanUpAlbums(Account account, File file) async { Future<void> _cleanUpAlbums(Account account, File file) async {
final albumRepo = this.albumRepo!;
final albums = (await ListAlbum(fileRepo, albumRepo)(account) final albums = (await ListAlbum(fileRepo, albumRepo)(account)
.where((event) => event is Album) .where((event) => event is Album)
.toList()).cast<Album>(); .toList()).cast<Album>();
@ -58,7 +59,7 @@ class Remove {
} }
final FileRepo fileRepo; final FileRepo fileRepo;
final AlbumRepo albumRepo; final AlbumRepo? albumRepo;
static final _log = Logger("use_case.remove.Remove"); static final _log = Logger("use_case.remove.Remove");
} }

View file

@ -44,18 +44,22 @@ class ResyncAlbum {
Future<AlbumFileItem> _syncOne(Account account, AlbumFileItem item, Future<AlbumFileItem> _syncOne(Account account, AlbumFileItem item,
ObjectStore objStore, Index index) async { ObjectStore objStore, Index index) async {
Map dbItem; Map? dbItem;
if (item.file.fileId != null) { if (item.file.fileId != null) {
final List dbItems = await index final List dbItems = await index
.getAll(AppDbFileDbEntry.toNamespacedFileId(account, item.file)); .getAll(AppDbFileDbEntry.toNamespacedFileId(account, item.file));
// find the one owned by us // find the one owned by us
try {
dbItem = dbItems.firstWhere((element) { dbItem = dbItems.firstWhere((element) {
final e = AppDbFileDbEntry.fromJson(element.cast<String, dynamic>()); final e = AppDbFileDbEntry.fromJson(element.cast<String, dynamic>());
return file_util.getUserDirName(e.file) == account.username; return file_util.getUserDirName(e.file) == account.username;
}, orElse: () => null); });
} on StateError catch (_) {
// not found
}
} else { } else {
dbItem = await objStore dbItem = await objStore
.getObject(AppDbFileDbEntry.toPrimaryKey(account, item.file)); .getObject(AppDbFileDbEntry.toPrimaryKey(account, item.file)) as Map;
} }
if (dbItem == null) { if (dbItem == null) {
_log.warning( _log.warning(

View file

@ -40,8 +40,9 @@ class UpdateDynamicAlbumCover {
} }
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) { Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
final coverFile = sortedFiles.firstWhere((element) => element.hasPreview); try {
if (coverFile != null) { final coverFile =
sortedFiles.firstWhere((element) => element.hasPreview ?? false);
// cache the result for later use // cache the result for later use
if (coverFile.path != if (coverFile.path !=
(album.coverProvider as AlbumAutoCoverProvider).coverFile?.path) { (album.coverProvider as AlbumAutoCoverProvider).coverFile?.path) {
@ -51,6 +52,8 @@ class UpdateDynamicAlbumCover {
), ),
); );
} }
} on StateError catch (_) {
// no files
} }
return album; return album;
} }

View file

@ -37,7 +37,7 @@ class UpdateDynamicAlbumTime {
} }
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) { Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
DateTime latestItemTime; DateTime? latestItemTime;
try { try {
latestItemTime = sortedFiles.first.bestDateTime; latestItemTime = sortedFiles.first.bestDateTime;
} catch (_) { } catch (_) {

View file

@ -40,8 +40,8 @@ class UpdateMissingMetadata {
_log.fine("[call] Updating metadata for ${file.path}"); _log.fine("[call] Updating metadata for ${file.path}");
final binary = await GetFileBinary(fileRepo)(account, file); final binary = await GetFileBinary(fileRepo)(account, file);
final metadata = await LoadMetadata()(account, file, binary); final metadata = await LoadMetadata()(account, file, binary);
int imageWidth, imageHeight; int? imageWidth, imageHeight;
Exif exif; Exif? exif;
if (metadata.containsKey("resolution")) { if (metadata.containsKey("resolution")) {
imageWidth = metadata["resolution"]["width"]; imageWidth = metadata["resolution"]["width"];
imageHeight = metadata["resolution"]["height"]; imageHeight = metadata["resolution"]["height"];

View file

@ -12,9 +12,9 @@ class UpdateProperty {
Future<void> call( Future<void> call(
Account account, Account account,
File file, { File file, {
OrNull<Metadata> metadata, OrNull<Metadata>? metadata,
OrNull<bool> isArchived, OrNull<bool>? isArchived,
OrNull<DateTime> overrideDateTime, OrNull<DateTime>? overrideDateTime,
}) async { }) async {
if (metadata == null && isArchived == null && overrideDateTime == null) { if (metadata == null && isArchived == null && overrideDateTime == null) {
// ? // ?
@ -22,9 +22,9 @@ class UpdateProperty {
return; return;
} }
if (metadata?.obj != null && metadata.obj.fileEtag != file.etag) { if (metadata?.obj != null && metadata!.obj!.fileEtag != file.etag) {
_log.warning( _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( await fileRepo.updateProperty(
account, account,

View file

@ -1,4 +1,4 @@
import 'package:idb_shim/idb_browser.dart'; import 'package:idb_shim/idb_browser.dart';
import 'package:idb_shim/idb_shim.dart'; import 'package:idb_shim/idb_shim.dart';
IdbFactory getDbFactory() => getIdbFactory(); IdbFactory getDbFactory() => idbFactoryBrowser;

View file

@ -8,9 +8,9 @@ import 'package:tuple/tuple.dart';
class Map extends StatefulWidget { class Map extends StatefulWidget {
const Map({ const Map({
Key key, Key? key,
this.center, required this.center,
this.zoom, required this.zoom,
this.onTap, this.onTap,
}) : super(key: key); }) : super(key: key);
@ -20,7 +20,7 @@ class Map extends StatefulWidget {
/// A pair of latitude and longitude coordinates, stored as degrees /// A pair of latitude and longitude coordinates, stored as degrees
final Tuple2<double, double> center; final Tuple2<double, double> center;
final double zoom; final double zoom;
final void Function() onTap; final void Function()? onTap;
} }
class _MapState extends State<Map> { class _MapState extends State<Map> {

View file

@ -15,8 +15,8 @@ import 'package:nc_photos/widget/sign_in.dart';
/// A dialog that allows the user to switch between accounts /// A dialog that allows the user to switch between accounts
class AccountPickerDialog extends StatefulWidget { class AccountPickerDialog extends StatefulWidget {
AccountPickerDialog({ AccountPickerDialog({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
@override @override
@ -29,7 +29,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
@override @override
initState() { initState() {
super.initState(); super.initState();
_accounts = Pref.inst().getAccounts([]); _accounts = Pref.inst().getAccountsOr([]);
} }
@override @override
@ -48,7 +48,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
Icons.close, Icons.close,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
tooltip: AppLocalizations.of(context).deleteTooltip, tooltip: AppLocalizations.of(context)!.deleteTooltip,
onPressed: () => _onRemoveItemPressed(a), onPressed: () => _onRemoveItemPressed(a),
), ),
), ),
@ -63,7 +63,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
..pushNamed(SignIn.routeName); ..pushNamed(SignIn.routeName);
}, },
child: Tooltip( child: Tooltip(
message: AppLocalizations.of(context).addServerTooltip, message: AppLocalizations.of(context)!.addServerTooltip,
child: Center( child: Center(
child: Icon( child: Icon(
Icons.add, Icons.add,
@ -89,7 +89,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
Icons.edit, Icons.edit,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
tooltip: AppLocalizations.of(context).editTooltip, tooltip: AppLocalizations.of(context)!.editTooltip,
onPressed: () => _onEditPressed(), onPressed: () => _onEditPressed(),
), ),
), ),
@ -106,20 +106,28 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
} }
void _onRemoveItemPressed(Account account) { void _onRemoveItemPressed(Account account) {
try {
_removeAccount(account); _removeAccount(account);
setState(() { setState(() {
_accounts = Pref.inst().getAccounts([]); _accounts = Pref.inst().getAccounts()!;
}); });
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.removeServerSuccessNotification(account.url)), .removeServerSuccessNotification(account.url)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} catch (e) {
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e, context)),
duration: k.snackBarDurationNormal,
));
}
} }
void _onEditPressed() async { void _onEditPressed() async {
try { try {
final result = await Navigator.of(context).pushNamed(RootPicker.routeName, final result = await Navigator.of(context).pushNamed<Account>(
RootPicker.routeName,
arguments: RootPickerArguments(widget.account)); arguments: RootPickerArguments(widget.account));
if (result != null) { if (result != null) {
// we've got a good account // we've got a good account
@ -129,19 +137,19 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
Navigator.of(context).pop(); Navigator.of(context).pop();
return; return;
} }
final accounts = Pref.inst().getAccounts([]); final accounts = Pref.inst().getAccounts()!;
if (accounts.contains(result)) { if (accounts.contains(result)) {
// conflict with another account. This normally won't happen because // conflict with another account. This normally won't happen because
// the app passwords are unique to each entry, but just in case // the app passwords are unique to each entry, but just in case
Navigator.of(context).pop(); Navigator.of(context).pop();
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.editAccountConflictFailureNotification), .editAccountConflictFailureNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
return; return;
} }
accounts[Pref.inst().getCurrentAccountIndex()] = result; accounts[Pref.inst().getCurrentAccountIndex()!] = result;
Pref.inst()..setAccounts(accounts); Pref.inst()..setAccounts(accounts);
Navigator.pushNamedAndRemoveUntil( Navigator.pushNamedAndRemoveUntil(
context, Home.routeName, (route) => false, context, Home.routeName, (route) => false,
@ -158,9 +166,9 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
} }
void _removeAccount(Account account) { void _removeAccount(Account account) {
final currentAccounts = Pref.inst().getAccounts([]); final currentAccounts = Pref.inst().getAccounts()!;
final currentAccount = final currentAccount =
currentAccounts[Pref.inst().getCurrentAccountIndex()]; currentAccounts[Pref.inst().getCurrentAccountIndex()!];
final newAccounts = final newAccounts =
currentAccounts.where((element) => element != account).toList(); currentAccounts.where((element) => element != account).toList();
final newAccountIndex = newAccounts.indexOf(currentAccount); final newAccountIndex = newAccounts.indexOf(currentAccount);
@ -172,7 +180,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
..setCurrentAccountIndex(newAccountIndex); ..setCurrentAccountIndex(newAccountIndex);
} }
List<Account> _accounts; late List<Account> _accounts;
static final _log = static final _log =
Logger("widget.account_picker_dialog._AccountPickerDialogState"); Logger("widget.account_picker_dialog._AccountPickerDialogState");

View file

@ -21,12 +21,17 @@ class AlbumDirPickerArguments {
class AlbumDirPicker extends StatefulWidget { class AlbumDirPicker extends StatefulWidget {
static const routeName = "/album-dir-picker"; static const routeName = "/album-dir-picker";
static Route buildRoute(AlbumDirPickerArguments args) =>
MaterialPageRoute<List<File>>(
builder: (context) => AlbumDirPicker.fromArgs(args),
);
AlbumDirPicker({ AlbumDirPicker({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
AlbumDirPicker.fromArgs(AlbumDirPickerArguments args, {Key key}) AlbumDirPicker.fromArgs(AlbumDirPickerArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -81,7 +86,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
child: Column( child: Column(
children: [ children: [
Text( Text(
AppLocalizations.of(context).albumDirPickerHeaderText, AppLocalizations.of(context)!.albumDirPickerHeaderText,
style: Theme.of(context).textTheme.headline5, style: Theme.of(context).textTheme.headline5,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -89,7 +94,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
Align( Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: Text( child: Text(
AppLocalizations.of(context).albumDirPickerSubHeaderText, AppLocalizations.of(context)!.albumDirPickerSubHeaderText,
), ),
), ),
], ],
@ -112,7 +117,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
), ),
ElevatedButton( ElevatedButton(
onPressed: () => _onConfirmPressed(context), 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) { if (picked.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).albumDirPickerListEmptyNotification), AppLocalizations.of(context)!.albumDirPickerListEmptyNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} else { } else {

View file

@ -5,9 +5,9 @@ import 'package:nc_photos/widget/selectable.dart';
class AlbumGridItem extends StatelessWidget { class AlbumGridItem extends StatelessWidget {
AlbumGridItem({ AlbumGridItem({
Key key, Key? key,
@required this.cover, required this.cover,
@required this.title, required this.title,
this.subtitle, this.subtitle,
this.subtitle2, this.subtitle2,
this.icon, this.icon,
@ -50,8 +50,8 @@ class AlbumGridItem extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
title ?? "", title,
style: Theme.of(context).textTheme.bodyText1.copyWith( style: Theme.of(context).textTheme.bodyText1!.copyWith(
color: AppTheme.getPrimaryTextColor(context), color: AppTheme.getPrimaryTextColor(context),
), ),
textAlign: TextAlign.start, textAlign: TextAlign.start,
@ -64,7 +64,7 @@ class AlbumGridItem extends StatelessWidget {
Expanded( Expanded(
child: Text( child: Text(
subtitle ?? "", subtitle ?? "",
style: Theme.of(context).textTheme.bodyText2.copyWith( style: Theme.of(context).textTheme.bodyText2!.copyWith(
fontSize: 10, fontSize: 10,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
@ -75,8 +75,8 @@ class AlbumGridItem extends StatelessWidget {
), ),
if (subtitle2?.isNotEmpty == true) if (subtitle2?.isNotEmpty == true)
Text( Text(
subtitle2, subtitle2!,
style: Theme.of(context).textTheme.bodyText2.copyWith( style: Theme.of(context).textTheme.bodyText2!.copyWith(
fontSize: 10, fontSize: 10,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
@ -93,12 +93,12 @@ class AlbumGridItem extends StatelessWidget {
final Widget cover; final Widget cover;
final String title; final String title;
final String subtitle; final String? subtitle;
/// Appears after [subtitle], aligned to the end side of parent /// Appears after [subtitle], aligned to the end side of parent
final String subtitle2; final String? subtitle2;
final IconData icon; final IconData? icon;
final bool isSelected; final bool isSelected;
final VoidCallback onTap; final VoidCallback? onTap;
final VoidCallback onLongPress; final VoidCallback? onLongPress;
} }

View file

@ -34,12 +34,16 @@ class AlbumImporterArguments {
class AlbumImporter extends StatefulWidget { class AlbumImporter extends StatefulWidget {
static const routeName = "/album-importer"; static const routeName = "/album-importer";
static Route buildRoute(AlbumImporterArguments args) => MaterialPageRoute(
builder: (context) => AlbumImporter.fromArgs(args),
);
AlbumImporter({ AlbumImporter({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
AlbumImporter.fromArgs(AlbumImporterArguments args, {Key key}) AlbumImporter.fromArgs(AlbumImporterArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -98,7 +102,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
child: Column( child: Column(
children: [ children: [
Text( Text(
AppLocalizations.of(context).albumImporterHeaderText, AppLocalizations.of(context)!.albumImporterHeaderText,
style: Theme.of(context).textTheme.headline5, style: Theme.of(context).textTheme.headline5,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -106,7 +110,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
Align( Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: Text( child: Text(
AppLocalizations.of(context).albumImporterSubHeaderText, AppLocalizations.of(context)!.albumImporterSubHeaderText,
), ),
), ),
], ],
@ -141,7 +145,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
), ),
ElevatedButton( ElevatedButton(
onPressed: () => _onImportPressed(context), 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, barrierDismissible: false,
context: context, context: context,
builder: (context) => ProcessingDialog( builder: (context) => ProcessingDialog(
text: AppLocalizations.of(context).albumImporterProgressText), text: AppLocalizations.of(context)!.albumImporterProgressText),
); );
try { try {
await _createAllAlbums(context); await _createAllAlbums(context);
@ -269,7 +273,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
.toList(); .toList();
} }
ListImportableAlbumBloc _bloc; late ListImportableAlbumBloc _bloc;
var _backingFiles = <File>[]; var _backingFiles = <File>[];
final _picks = <File>[]; final _picks = <File>[];

View file

@ -18,8 +18,8 @@ import 'package:tuple/tuple.dart';
class AlbumPickerDialog extends StatefulWidget { class AlbumPickerDialog extends StatefulWidget {
AlbumPickerDialog({ AlbumPickerDialog({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
@override @override
@ -68,7 +68,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
_reqQuery(); _reqQuery();
} else { } else {
// process the current state // process the current state
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
setState(() { setState(() {
_onStateChange(context, _bloc.state); _onStateChange(context, _bloc.state);
}); });
@ -81,7 +81,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
SimpleDialogOption( SimpleDialogOption(
onPressed: () => _onNewAlbumPressed(context), onPressed: () => _onNewAlbumPressed(context),
child: Tooltip( child: Tooltip(
message: AppLocalizations.of(context).createAlbumTooltip, message: AppLocalizations.of(context)!.createAlbumTooltip,
child: Center( child: Center(
child: Icon( child: Icon(
Icons.add, Icons.add,
@ -170,7 +170,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
_bloc.add(ListAlbumBlocQuery(widget.account)); _bloc.add(ListAlbumBlocQuery(widget.account));
} }
ListAlbumBloc _bloc; late ListAlbumBloc _bloc;
final _items = <Album>[]; final _items = <Album>[];

View file

@ -18,7 +18,7 @@ class AlbumSearchDelegate extends SearchDelegate {
AlbumSearchDelegate(BuildContext context, this.account) AlbumSearchDelegate(BuildContext context, this.account)
: super( : super(
searchFieldLabel: searchFieldLabel:
AppLocalizations.of(context).albumSearchTextFieldHint, AppLocalizations.of(context)!.albumSearchTextFieldHint,
) { ) {
final fileRepo = FileRepo(FileCachedDataSource()); final fileRepo = FileRepo(FileCachedDataSource());
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());
@ -38,7 +38,7 @@ class AlbumSearchDelegate extends SearchDelegate {
return [ return [
IconButton( IconButton(
icon: Icon(Icons.clear), icon: Icon(Icons.clear),
tooltip: AppLocalizations.of(context).clearTooltip, tooltip: AppLocalizations.of(context)!.clearTooltip,
onPressed: () { onPressed: () {
query = ""; query = "";
}, },
@ -88,7 +88,7 @@ class AlbumSearchDelegate extends SearchDelegate {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
AppLocalizations.of(context).listNoResultsText, AppLocalizations.of(context)!.listNoResultsText,
style: const TextStyle(fontSize: 24), style: const TextStyle(fontSize: 24),
), ),
], ],

View file

@ -42,13 +42,17 @@ class AlbumViewerArguments {
class AlbumViewer extends StatefulWidget { class AlbumViewer extends StatefulWidget {
static const routeName = "/album-viewer"; static const routeName = "/album-viewer";
static Route buildRoute(AlbumViewerArguments args) => MaterialPageRoute(
builder: (context) => AlbumViewer.fromArgs(args),
);
AlbumViewer({ AlbumViewer({
Key key, Key? key,
@required this.account, required this.account,
@required this.album, required this.album,
}) : super(key: key); }) : super(key: key);
AlbumViewer.fromArgs(AlbumViewerArguments args, {Key key}) AlbumViewer.fromArgs(AlbumViewerArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -96,7 +100,7 @@ class _AlbumViewerState extends State<AlbumViewer>
@override @override
enterEditMode() { enterEditMode() {
super.enterEditMode(); super.enterEditMode();
_editAlbum = _album.copyWith(); _editAlbum = _album!.copyWith();
setState(() { setState(() {
_transformItems(); _transformItems();
}); });
@ -104,7 +108,7 @@ class _AlbumViewerState extends State<AlbumViewer>
if (!SessionStorage().hasShowDragRearrangeNotification) { if (!SessionStorage().hasShowDragRearrangeNotification) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).albumEditDragRearrangeNotification), AppLocalizations.of(context)!.albumEditDragRearrangeNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
SessionStorage().hasShowDragRearrangeNotification = true; SessionStorage().hasShowDragRearrangeNotification = true;
@ -112,15 +116,15 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
@override @override
validateEditMode() => _editFormKey?.currentState?.validate() == true; validateEditMode() => _editFormKey.currentState?.validate() == true;
@override @override
doneEditMode() { doneEditMode() {
try { try {
// persist the changes // persist the changes
_editFormKey.currentState.save(); _editFormKey.currentState!.save();
final newAlbum = makeEdited(_editAlbum); final newAlbum = makeEdited(_editAlbum!);
if (newAlbum.copyWith(lastUpdated: OrNull(_album.lastUpdated)) != if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
_album) { _album) {
_log.info("[doneEditMode] Album modified: $newAlbum"); _log.info("[doneEditMode] Album modified: $newAlbum");
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());
@ -217,7 +221,7 @@ class _AlbumViewerState extends State<AlbumViewer>
} else if (isSelectionMode) { } else if (isSelectionMode) {
return _buildSelectionAppBar(context); return _buildSelectionAppBar(context);
} else { } 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) if (platform_k.isAndroid)
IconButton( IconButton(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
tooltip: AppLocalizations.of(context).shareSelectedTooltip, tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
onPressed: () { onPressed: () {
_onSelectionAppBarSharePressed(context); _onSelectionAppBarSharePressed(context);
}, },
), ),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
tooltip: AppLocalizations.of(context).removeSelectedFromAlbumTooltip, tooltip: AppLocalizations.of(context)!.removeSelectedFromAlbumTooltip,
onPressed: () { onPressed: () {
_onSelectionAppBarRemovePressed(); _onSelectionAppBarRemovePressed();
}, },
@ -245,12 +249,12 @@ class _AlbumViewerState extends State<AlbumViewer>
return buildEditAppBar(context, widget.account, widget.album, actions: [ return buildEditAppBar(context, widget.account, widget.album, actions: [
IconButton( IconButton(
icon: Icon(Icons.text_fields), icon: Icon(Icons.text_fields),
tooltip: AppLocalizations.of(context).albumAddTextTooltip, tooltip: AppLocalizations.of(context)!.albumAddTextTooltip,
onPressed: _onEditAppBarAddTextPressed, onPressed: _onEditAppBarAddTextPressed,
), ),
IconButton( IconButton(
icon: Icon(Icons.sort_by_alpha), icon: Icon(Icons.sort_by_alpha),
tooltip: AppLocalizations.of(context).sortTooltip, tooltip: AppLocalizations.of(context)!.sortTooltip,
onPressed: _onEditAppBarSortPressed, onPressed: _onEditAppBarSortPressed,
), ),
]); ]);
@ -279,7 +283,7 @@ class _AlbumViewerState extends State<AlbumViewer>
if (selected.isEmpty) { if (selected.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: content:
Text(AppLocalizations.of(context).shareSelectedEmptyNotification), Text(AppLocalizations.of(context)!.shareSelectedEmptyNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
return; return;
@ -301,14 +305,14 @@ class _AlbumViewerState extends State<AlbumViewer>
.toList(); .toList();
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());
final newAlbum = _album.copyWith( final newAlbum = _album!.copyWith(
provider: AlbumStaticProvider( provider: AlbumStaticProvider(
items: newItems, items: newItems,
), ),
); );
UpdateAlbum(albumRepo)(widget.account, newAlbum).then((_) { UpdateAlbum(albumRepo)(widget.account, newAlbum).then((_) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.removeSelectedFromAlbumSuccessNotification( .removeSelectedFromAlbumSuccessNotification(
selectedIndexes.length)), selectedIndexes.length)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
@ -323,7 +327,7 @@ class _AlbumViewerState extends State<AlbumViewer>
stacktrace); stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
"${AppLocalizations.of(context).removeSelectedFromAlbumFailureNotification}: " "${AppLocalizations.of(context)!.removeSelectedFromAlbumFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -334,14 +338,14 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
void _onEditAppBarSortPressed() { void _onEditAppBarSortPressed() {
final sortProvider = _editAlbum.sortProvider; final sortProvider = _editAlbum!.sortProvider;
showDialog( showDialog(
context: context, context: context,
builder: (context) => FancyOptionPicker( builder: (context) => FancyOptionPicker(
title: AppLocalizations.of(context).sortOptionDialogTitle, title: AppLocalizations.of(context)!.sortOptionDialogTitle,
items: [ items: [
FancyOptionPickerItem( FancyOptionPickerItem(
label: AppLocalizations.of(context).sortOptionTimeAscendingLabel, label: AppLocalizations.of(context)!.sortOptionTimeAscendingLabel,
isSelected: sortProvider is AlbumTimeSortProvider && isSelected: sortProvider is AlbumTimeSortProvider &&
sortProvider.isAscending, sortProvider.isAscending,
onSelect: () { onSelect: () {
@ -350,7 +354,7 @@ class _AlbumViewerState extends State<AlbumViewer>
}, },
), ),
FancyOptionPickerItem( FancyOptionPickerItem(
label: AppLocalizations.of(context).sortOptionTimeDescendingLabel, label: AppLocalizations.of(context)!.sortOptionTimeDescendingLabel,
isSelected: sortProvider is AlbumTimeSortProvider && isSelected: sortProvider is AlbumTimeSortProvider &&
!sortProvider.isAscending, !sortProvider.isAscending,
onSelect: () { onSelect: () {
@ -364,7 +368,7 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
void _onSortOldestPressed() { void _onSortOldestPressed() {
_editAlbum = _editAlbum.copyWith( _editAlbum = _editAlbum!.copyWith(
sortProvider: AlbumTimeSortProvider(isAscending: true), sortProvider: AlbumTimeSortProvider(isAscending: true),
); );
setState(() { setState(() {
@ -373,7 +377,7 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
void _onSortNewestPressed() { void _onSortNewestPressed() {
_editAlbum = _editAlbum.copyWith( _editAlbum = _editAlbum!.copyWith(
sortProvider: AlbumTimeSortProvider(isAscending: false), sortProvider: AlbumTimeSortProvider(isAscending: false),
); );
setState(() { setState(() {
@ -431,7 +435,7 @@ class _AlbumViewerState extends State<AlbumViewer>
final newIndex = final newIndex =
toIndex + (isBefore ? 0 : 1) + (fromIndex < toIndex ? -1 : 0); toIndex + (isBefore ? 0 : 1) + (fromIndex < toIndex ? -1 : 0);
_sortedItems.insert(newIndex, item); _sortedItems.insert(newIndex, item);
_editAlbum = _editAlbum.copyWith( _editAlbum = _editAlbum!.copyWith(
sortProvider: AlbumNullSortProvider(), sortProvider: AlbumNullSortProvider(),
// save the current order // save the current order
provider: AlbumStaticProvider( provider: AlbumStaticProvider(
@ -444,14 +448,14 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
void _onEditAppBarAddTextPressed() { void _onEditAppBarAddTextPressed() {
showDialog( showDialog<String>(
context: context, context: context,
builder: (context) => SimpleInputDialog(), builder: (context) => SimpleInputDialog(),
).then((value) { ).then((value) {
if (value == null) { if (value == null) {
return; return;
} }
_editAlbum = _editAlbum.copyWith( _editAlbum = _editAlbum!.copyWith(
provider: AlbumStaticProvider( provider: AlbumStaticProvider(
items: [ items: [
AlbumLabelItem(text: value), AlbumLabelItem(text: value),
@ -466,7 +470,7 @@ class _AlbumViewerState extends State<AlbumViewer>
} }
void _onLabelItemEditPressed(AlbumLabelItem item, int index) { void _onLabelItemEditPressed(AlbumLabelItem item, int index) {
showDialog( showDialog<String>(
context: context, context: context,
builder: (context) => SimpleInputDialog( builder: (context) => SimpleInputDialog(
initialText: item.text, initialText: item.text,
@ -476,7 +480,7 @@ class _AlbumViewerState extends State<AlbumViewer>
return; return;
} }
_sortedItems[index] = AlbumLabelItem(text: value); _sortedItems[index] = AlbumLabelItem(text: value);
_editAlbum = _editAlbum.copyWith( _editAlbum = _editAlbum!.copyWith(
provider: AlbumStaticProvider( provider: AlbumStaticProvider(
items: _sortedItems, items: _sortedItems,
), ),
@ -490,9 +494,10 @@ class _AlbumViewerState extends State<AlbumViewer>
void _transformItems() { void _transformItems() {
if (_editAlbum != null) { if (_editAlbum != null) {
// edit mode // edit mode
_sortedItems = _editAlbum.sortProvider.sort(_getAlbumItemsOf(_editAlbum)); _sortedItems =
_editAlbum!.sortProvider.sort(_getAlbumItemsOf(_editAlbum!));
} else { } else {
_sortedItems = _album.sortProvider.sort(_getAlbumItemsOf(_album)); _sortedItems = _album!.sortProvider.sort(_getAlbumItemsOf(_album!));
} }
_backingFiles = _sortedItems _backingFiles = _sortedItems
.whereType<AlbumFileItem>() .whereType<AlbumFileItem>()
@ -644,29 +649,29 @@ class _AlbumViewerState extends State<AlbumViewer>
static List<AlbumItem> _getAlbumItemsOf(Album a) => static List<AlbumItem> _getAlbumItemsOf(Album a) =>
AlbumStaticProvider.of(a).items; AlbumStaticProvider.of(a).items;
Album _album; Album? _album;
var _sortedItems = <AlbumItem>[]; var _sortedItems = <AlbumItem>[];
var _backingFiles = <File>[]; var _backingFiles = <File>[];
final _scrollController = ScrollController(); final _scrollController = ScrollController();
double _itemListMaxExtent; double? _itemListMaxExtent;
bool _isDragging = false; bool _isDragging = false;
// == null if not drag scrolling // == null if not drag scrolling
bool _isDragScrollingDown; bool? _isDragScrollingDown;
final _editFormKey = GlobalKey<FormState>(); final _editFormKey = GlobalKey<FormState>();
Album _editAlbum; Album? _editAlbum;
static final _log = Logger("widget.album_viewer._AlbumViewerState"); static final _log = Logger("widget.album_viewer._AlbumViewerState");
} }
abstract class _ListItem implements SelectableItem, DraggableItem { abstract class _ListItem implements SelectableItem, DraggableItem {
_ListItem({ _ListItem({
@required this.index, required this.index,
VoidCallback onTap, VoidCallback? onTap,
DragTargetAccept<DraggableItem> onDropBefore, DragTargetAccept<DraggableItem>? onDropBefore,
DragTargetAccept<DraggableItem> onDropAfter, DragTargetAccept<DraggableItem>? onDropAfter,
VoidCallback onDragStarted, VoidCallback? onDragStarted,
VoidCallback onDragEndedAny, VoidCallback? onDragEndedAny,
}) : _onTap = onTap, }) : _onTap = onTap,
_onDropBefore = onDropBefore, _onDropBefore = onDropBefore,
_onDropAfter = onDropAfter, _onDropAfter = onDropAfter,
@ -709,22 +714,22 @@ abstract class _ListItem implements SelectableItem, DraggableItem {
final int index; final int index;
final VoidCallback _onTap; final VoidCallback? _onTap;
final DragTargetAccept<DraggableItem> _onDropBefore; final DragTargetAccept<DraggableItem>? _onDropBefore;
final DragTargetAccept<DraggableItem> _onDropAfter; final DragTargetAccept<DraggableItem>? _onDropAfter;
final VoidCallback _onDragStarted; final VoidCallback? _onDragStarted;
final VoidCallback _onDragEndedAny; final VoidCallback? _onDragEndedAny;
} }
abstract class _FileListItem extends _ListItem { abstract class _FileListItem extends _ListItem {
_FileListItem({ _FileListItem({
@required int index, required int index,
@required this.file, required this.file,
VoidCallback onTap, VoidCallback? onTap,
DragTargetAccept<DraggableItem> onDropBefore, DragTargetAccept<DraggableItem>? onDropBefore,
DragTargetAccept<DraggableItem> onDropAfter, DragTargetAccept<DraggableItem>? onDropAfter,
VoidCallback onDragStarted, VoidCallback? onDragStarted,
VoidCallback onDragEndedAny, VoidCallback? onDragEndedAny,
}) : super( }) : super(
index: index, index: index,
onTap: onTap, onTap: onTap,
@ -739,15 +744,15 @@ abstract class _FileListItem extends _ListItem {
class _ImageListItem extends _FileListItem { class _ImageListItem extends _FileListItem {
_ImageListItem({ _ImageListItem({
@required int index, required int index,
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
DragTargetAccept<DraggableItem> onDropBefore, DragTargetAccept<DraggableItem>? onDropBefore,
DragTargetAccept<DraggableItem> onDropAfter, DragTargetAccept<DraggableItem>? onDropAfter,
VoidCallback onDragStarted, VoidCallback? onDragStarted,
VoidCallback onDragEndedAny, VoidCallback? onDragEndedAny,
}) : super( }) : super(
index: index, index: index,
file: file, file: file,
@ -773,15 +778,15 @@ class _ImageListItem extends _FileListItem {
class _VideoListItem extends _FileListItem { class _VideoListItem extends _FileListItem {
_VideoListItem({ _VideoListItem({
@required int index, required int index,
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
DragTargetAccept<DraggableItem> onDropBefore, DragTargetAccept<DraggableItem>? onDropBefore,
DragTargetAccept<DraggableItem> onDropAfter, DragTargetAccept<DraggableItem>? onDropAfter,
VoidCallback onDragStarted, VoidCallback? onDragStarted,
VoidCallback onDragEndedAny, VoidCallback? onDragEndedAny,
}) : super( }) : super(
index: index, index: index,
file: file, file: file,
@ -806,12 +811,12 @@ class _VideoListItem extends _FileListItem {
class _LabelListItem extends _ListItem { class _LabelListItem extends _ListItem {
_LabelListItem({ _LabelListItem({
@required int index, required int index,
@required this.text, required this.text,
DragTargetAccept<DraggableItem> onDropBefore, DragTargetAccept<DraggableItem>? onDropBefore,
DragTargetAccept<DraggableItem> onDropAfter, DragTargetAccept<DraggableItem>? onDropAfter,
VoidCallback onDragStarted, VoidCallback? onDragStarted,
VoidCallback onDragEndedAny, VoidCallback? onDragEndedAny,
}) : super( }) : super(
index: index, index: index,
onDropBefore: onDropBefore, onDropBefore: onDropBefore,
@ -842,13 +847,13 @@ class _LabelListItem extends _ListItem {
class _EditLabelListItem extends _LabelListItem { class _EditLabelListItem extends _LabelListItem {
_EditLabelListItem({ _EditLabelListItem({
@required int index, required int index,
@required String text, required String text,
@required this.onEditPressed, required this.onEditPressed,
DragTargetAccept<DraggableItem> onDropBefore, DragTargetAccept<DraggableItem>? onDropBefore,
DragTargetAccept<DraggableItem> onDropAfter, DragTargetAccept<DraggableItem>? onDropAfter,
VoidCallback onDragStarted, VoidCallback? onDragStarted,
VoidCallback onDragEndedAny, VoidCallback? onDragEndedAny,
}) : super( }) : super(
index: index, index: index,
text: text, text: text,
@ -873,7 +878,7 @@ class _EditLabelListItem extends _LabelListItem {
end: 0, end: 0,
child: IconButton( child: IconButton(
icon: Icon(Icons.edit), icon: Icon(Icons.edit),
tooltip: AppLocalizations.of(context).editTooltip, tooltip: AppLocalizations.of(context)!.editTooltip,
onPressed: onEditPressed, onPressed: onEditPressed,
), ),
), ),
@ -886,5 +891,5 @@ class _EditLabelListItem extends _LabelListItem {
return super.buildWidget(context); return super.buildWidget(context);
} }
final VoidCallback onEditPressed; final VoidCallback? onEditPressed;
} }

View file

@ -18,20 +18,17 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
@override @override
initState() { initState() {
super.initState(); super.initState();
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0); _thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevelOr(0);
} }
@protected @protected
File initCover(Account account, List<File> backingFiles) { void initCover(Account account, List<File> backingFiles) {
try { try {
final coverFile = final coverFile =
backingFiles.firstWhere((element) => element.hasPreview); backingFiles.firstWhere((element) => element.hasPreview ?? false);
_coverPreviewUrl = api_util.getFilePreviewUrl(account, coverFile, _coverPreviewUrl = api_util.getFilePreviewUrl(account, coverFile,
width: 1024, height: 600); width: 1024, height: 600);
return coverFile; } catch (_) {}
} catch (_) {
return null;
}
} }
@protected @protected
@ -39,9 +36,9 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
BuildContext context, BuildContext context,
Account account, Account account,
Album album, { Album album, {
List<Widget> actions, List<Widget>? actions,
List<PopupMenuEntry<int>> Function(BuildContext) menuItemBuilder, List<PopupMenuEntry<int>> Function(BuildContext)? menuItemBuilder,
void Function(int) onSelectedMenuItem, void Function(int)? onSelectedMenuItem,
}) { }) {
return SliverAppBar( return SliverAppBar(
floating: true, floating: true,
@ -58,7 +55,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
icon: const Icon(Icons.photo_size_select_large), icon: const Icon(Icons.photo_size_select_large),
tooltip: AppLocalizations.of(context).zoomTooltip, tooltip: AppLocalizations.of(context)!.zoomTooltip,
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuZoom( PopupMenuZoom(
initialValue: _thumbZoomLevel, initialValue: _thumbZoomLevel,
@ -74,12 +71,12 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
], ],
), ),
...(actions ?? []), ...(actions ?? []),
PopupMenuButton( PopupMenuButton<int>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip, tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: -1, value: -1,
child: Text(AppLocalizations.of(context).editAlbumMenuLabel), child: Text(AppLocalizations.of(context)!.editAlbumMenuLabel),
), ),
...(menuItemBuilder?.call(context) ?? []), ...(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)), .selectionAppBarTitle(selectedListItems.length)),
actions: actions, actions: actions,
), ),
@ -132,7 +129,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
BuildContext context, BuildContext context,
Account account, Account account,
Album album, { Album album, {
List<Widget> actions, List<Widget>? actions,
}) { }) {
return SliverAppBar( return SliverAppBar(
floating: true, floating: true,
@ -141,16 +138,17 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
background: _getAppBarCover(context, account), background: _getAppBarCover(context, account),
title: TextFormField( title: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context).nameInputHint, hintText: AppLocalizations.of(context)!.nameInputHint,
), ),
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value?.isNotEmpty == true) {
return AppLocalizations.of(context).albumNameInputInvalidEmpty;
}
return null; return null;
} else {
return AppLocalizations.of(context)!.albumNameInputInvalidEmpty;
}
}, },
onSaved: (value) { onSaved: (value) {
_editFormValue.name = value; _editFormValue.name = value!;
}, },
onChanged: (value) { onChanged: (value) {
// need to save the value otherwise it'll return to the initial // need to save the value otherwise it'll return to the initial
@ -166,7 +164,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
tooltip: AppLocalizations.of(context).doneButtonTooltip, tooltip: AppLocalizations.of(context)!.doneButtonTooltip,
onPressed: () { onPressed: () {
if (validateEditMode()) { if (validateEditMode()) {
setState(() { setState(() {
@ -226,7 +224,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
}); });
} }
Widget _getAppBarCover(BuildContext context, Account account) { Widget? _getAppBarCover(BuildContext context, Account account) {
try { try {
if (_coverPreviewUrl != null) { if (_coverPreviewUrl != null) {
return Opacity( return Opacity(
@ -236,7 +234,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
fit: BoxFit.cover, fit: BoxFit.cover,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: _coverPreviewUrl, imageUrl: _coverPreviewUrl!,
httpHeaders: { httpHeaders: {
"Authorization": Api.getAuthorizationHeaderValue(account), "Authorization": Api.getAuthorizationHeaderValue(account),
}, },
@ -254,11 +252,11 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
return null; return null;
} }
String _coverPreviewUrl; String? _coverPreviewUrl;
var _thumbZoomLevel = 0; var _thumbZoomLevel = 0;
var _isEditMode = false; var _isEditMode = false;
String _editNameValue; String? _editNameValue;
var _editFormValue = _EditFormValue(); var _editFormValue = _EditFormValue();
static final _log = Logger("widget.album_viewer_mixin.AlbumViewerMixin"); static final _log = Logger("widget.album_viewer_mixin.AlbumViewerMixin");
@ -266,5 +264,5 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
} }
class _EditFormValue { class _EditFormValue {
String name; late String name;
} }

View file

@ -5,14 +5,14 @@ import 'package:flutter/widgets.dart';
/// The point is to disable non-visible buttons /// The point is to disable non-visible buttons
class AnimatedVisibility extends StatefulWidget { class AnimatedVisibility extends StatefulWidget {
const AnimatedVisibility({ const AnimatedVisibility({
Key key, Key? key,
this.child, required this.child,
@required this.opacity, required this.opacity,
this.curve = Curves.linear, this.curve = Curves.linear,
@required this.duration, required this.duration,
this.onEnd, this.onEnd,
this.alwaysIncludeSemantics = false, this.alwaysIncludeSemantics = false,
}) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), }) : assert(opacity >= 0.0 && opacity <= 1.0),
super(key: key); super(key: key);
@override @override
@ -22,7 +22,7 @@ class AnimatedVisibility extends StatefulWidget {
final double opacity; final double opacity;
final Curve curve; final Curve curve;
final Duration duration; final Duration duration;
final VoidCallback onEnd; final VoidCallback? onEnd;
final bool alwaysIncludeSemantics; final bool alwaysIncludeSemantics;
} }

View file

@ -32,12 +32,16 @@ class ArchiveViewerArguments {
class ArchiveViewer extends StatefulWidget { class ArchiveViewer extends StatefulWidget {
static const routeName = "/archive-viewer"; static const routeName = "/archive-viewer";
static Route buildRoute(ArchiveViewerArguments args) => MaterialPageRoute(
builder: (context) => ArchiveViewer.fromArgs(args),
);
ArchiveViewer({ ArchiveViewer({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
ArchiveViewer.fromArgs(ArchiveViewerArguments args, {Key key}) ArchiveViewer.fromArgs(ArchiveViewerArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -55,7 +59,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
initState() { initState() {
super.initState(); super.initState();
_initBloc(); _initBloc();
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0); _thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevelOr(0);
} }
@override @override
@ -81,7 +85,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
_reqQuery(); _reqQuery();
} else { } else {
// process the current state // process the current state
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
setState(() { setState(() {
_onStateChange(context, _bloc.state); _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)), .selectionAppBarTitle(selectedListItems.length)),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.unarchive), icon: const Icon(Icons.unarchive),
tooltip: AppLocalizations.of(context).unarchiveSelectedTooltip, tooltip: AppLocalizations.of(context)!.unarchiveSelectedTooltip,
onPressed: () { onPressed: () {
_onSelectionAppBarUnarchivePressed(); _onSelectionAppBarUnarchivePressed();
}, },
@ -161,12 +165,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
Widget _buildNormalAppBar(BuildContext context) { Widget _buildNormalAppBar(BuildContext context) {
return SliverAppBar( return SliverAppBar(
title: Text(AppLocalizations.of(context).albumArchiveLabel), title: Text(AppLocalizations.of(context)!.albumArchiveLabel),
floating: true, floating: true,
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
icon: const Icon(Icons.photo_size_select_large), icon: const Icon(Icons.photo_size_select_large),
tooltip: AppLocalizations.of(context).zoomTooltip, tooltip: AppLocalizations.of(context)!.zoomTooltip,
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuZoom( PopupMenuZoom(
initialValue: _thumbZoomLevel, initialValue: _thumbZoomLevel,
@ -208,7 +212,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
Future<void> _onSelectionAppBarUnarchivePressed() async { Future<void> _onSelectionAppBarUnarchivePressed() async {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.unarchiveSelectedProcessingNotification(selectedListItems.length)), .unarchiveSelectedProcessingNotification(selectedListItems.length)),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
@ -237,12 +241,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
if (failures.isEmpty) { if (failures.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).unarchiveSelectedSuccessNotification), AppLocalizations.of(context)!.unarchiveSelectedSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.unarchiveSelectedFailureNotification(failures.length)), .unarchiveSelectedFailureNotification(failures.length)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -308,7 +312,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
} }
} }
ScanDirBloc _bloc; late ScanDirBloc _bloc;
var _backingFiles = <File>[]; var _backingFiles = <File>[];
@ -319,7 +323,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
abstract class _ListItem implements SelectableItem { abstract class _ListItem implements SelectableItem {
_ListItem({ _ListItem({
VoidCallback onTap, VoidCallback? onTap,
}) : _onTap = onTap; }) : _onTap = onTap;
@override @override
@ -331,13 +335,13 @@ abstract class _ListItem implements SelectableItem {
@override @override
get staggeredTile => const StaggeredTile.count(1, 1); get staggeredTile => const StaggeredTile.count(1, 1);
final VoidCallback _onTap; final VoidCallback? _onTap;
} }
abstract class _FileListItem extends _ListItem { abstract class _FileListItem extends _ListItem {
_FileListItem({ _FileListItem({
@required this.file, required this.file,
VoidCallback onTap, VoidCallback? onTap,
}) : super(onTap: onTap); }) : super(onTap: onTap);
@override @override
@ -353,10 +357,10 @@ abstract class _FileListItem extends _ListItem {
class _ImageListItem extends _FileListItem { class _ImageListItem extends _FileListItem {
_ImageListItem({ _ImageListItem({
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
}) : super(file: file, onTap: onTap); }) : super(file: file, onTap: onTap);
@override @override
@ -374,10 +378,10 @@ class _ImageListItem extends _FileListItem {
class _VideoListItem extends _FileListItem { class _VideoListItem extends _FileListItem {
_VideoListItem({ _VideoListItem({
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
}) : super(file: file, onTap: onTap); }) : super(file: file, onTap: onTap);
@override @override

View file

@ -13,8 +13,8 @@ import 'package:nc_photos/widget/album_grid_item.dart';
/// Build a standard [AlbumGridItem] for an [Album] /// Build a standard [AlbumGridItem] for an [Album]
class AlbumGridItemBuilder { class AlbumGridItemBuilder {
AlbumGridItemBuilder({ AlbumGridItemBuilder({
@required this.account, required this.account,
@required this.album, required this.album,
this.isSelected = false, this.isSelected = false,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
@ -22,9 +22,9 @@ class AlbumGridItemBuilder {
AlbumGridItem build(BuildContext context) { AlbumGridItem build(BuildContext context) {
var subtitle = ""; var subtitle = "";
String subtitle2; String? subtitle2;
if (album.provider is AlbumStaticProvider) { if (album.provider is AlbumStaticProvider) {
subtitle = AppLocalizations.of(context) subtitle = AppLocalizations.of(context)!
.albumSize(AlbumStaticProvider.of(album).items.length); .albumSize(AlbumStaticProvider.of(album).items.length);
} else if (album.provider is AlbumDirProvider) { } else if (album.provider is AlbumDirProvider) {
final provider = album.provider as AlbumDirProvider; final provider = album.provider as AlbumDirProvider;
@ -49,7 +49,7 @@ class AlbumGridItemBuilder {
Widget cover; Widget cover;
try { try {
final coverFile = album.coverProvider.getCover(album); final coverFile = album.coverProvider.getCover(album);
final previewUrl = api_util.getFilePreviewUrl(account, coverFile, final previewUrl = api_util.getFilePreviewUrl(account, coverFile!,
width: 512, height: 512); width: 512, height: 512);
cover = FittedBox( cover = FittedBox(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
@ -89,6 +89,6 @@ class AlbumGridItemBuilder {
final Account account; final Account account;
final Album album; final Album album;
final bool isSelected; final bool isSelected;
final VoidCallback onTap; final VoidCallback? onTap;
final VoidCallback onLongPress; final VoidCallback? onLongPress;
} }

View file

@ -21,8 +21,8 @@ class CachedNetworkImage extends StatelessWidget {
/// to clear the image from the [ImageCache]. /// to clear the image from the [ImageCache].
static Future evictFromCache( static Future evictFromCache(
String url, { String url, {
String cacheKey, String? cacheKey,
BaseCacheManager cacheManager, BaseCacheManager? cacheManager,
double scale = 1.0, double scale = 1.0,
}) async { }) async {
cacheManager = cacheManager ?? DefaultCacheManager(); cacheManager = cacheManager ?? DefaultCacheManager();
@ -33,31 +33,31 @@ class CachedNetworkImage extends StatelessWidget {
final CachedNetworkImageProvider _image; final CachedNetworkImageProvider _image;
/// Option to use cachemanager with other settings /// Option to use cachemanager with other settings
final BaseCacheManager cacheManager; final BaseCacheManager? cacheManager;
/// The target image that is displayed. /// The target image that is displayed.
final String imageUrl; final String imageUrl;
/// The target image's cache key. /// The target image's cache key.
final String cacheKey; final String? cacheKey;
/// Optional builder to further customize the display of the image. /// Optional builder to further customize the display of the image.
final ImageWidgetBuilder imageBuilder; final ImageWidgetBuilder? imageBuilder;
/// Widget displayed while the target [imageUrl] is loading. /// Widget displayed while the target [imageUrl] is loading.
final PlaceholderWidgetBuilder placeholder; final PlaceholderWidgetBuilder? placeholder;
/// Widget displayed while the target [imageUrl] is loading. /// Widget displayed while the target [imageUrl] is loading.
final ProgressIndicatorBuilder progressIndicatorBuilder; final ProgressIndicatorBuilder? progressIndicatorBuilder;
/// Widget displayed while the target [imageUrl] failed loading. /// Widget displayed while the target [imageUrl] failed loading.
final LoadingErrorWidgetBuilder errorWidget; final LoadingErrorWidgetBuilder? errorWidget;
/// The duration of the fade-in animation for the [placeholder]. /// 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]. /// 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]. /// The curve of the fade-out animation for the [placeholder].
final Curve fadeOutCurve; 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 /// 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 /// placeholder widget does not match that of the target image. The size is
/// also affected by the scale factor. /// also affected by the scale factor.
final double width; final double? width;
/// If non-null, require the image to have this height. /// 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 /// 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 /// placeholder widget does not match that of the target image. The size is
/// also affected by the scale factor. /// also affected by the scale factor.
final double height; final double? height;
/// How to inscribe the image into the space allocated during layout. /// How to inscribe the image into the space allocated during layout.
/// ///
/// The default varies based on the other fields. See the discussion at /// The default varies based on the other fields. See the discussion at
/// [paintImage]. /// [paintImage].
final BoxFit fit; final BoxFit? fit;
/// How to align the image within its bounds. /// How to align the image within its bounds.
/// ///
@ -112,7 +112,7 @@ class CachedNetworkImage extends StatelessWidget {
/// specify an [AlignmentGeometry]. /// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments /// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction. /// relative to text direction.
final AlignmentGeometry alignment; final Alignment alignment;
/// How to paint any portions of the layout bounds not covered by the image. /// How to paint any portions of the layout bounds not covered by the image.
final ImageRepeat repeat; final ImageRepeat repeat;
@ -135,14 +135,14 @@ class CachedNetworkImage extends StatelessWidget {
final bool matchTextDirection; final bool matchTextDirection;
/// Optional headers for the http request of the image url /// 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 /// When set to true it will animate from the old image to the new image
/// if the url changes. /// if the url changes.
final bool useOldImageOnUrlChange; final bool useOldImageOnUrlChange;
/// If non-null, this color is blended with each image pixel using [colorBlendMode]. /// 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. /// Used to combine [color] with this image.
/// ///
@ -152,7 +152,7 @@ class CachedNetworkImage extends StatelessWidget {
/// See also: /// See also:
/// ///
/// * [BlendMode], which includes an illustration of the effect of each blend mode. /// * [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. /// Target the interpolation quality for image scaling.
/// ///
@ -160,24 +160,24 @@ class CachedNetworkImage extends StatelessWidget {
final FilterQuality filterQuality; final FilterQuality filterQuality;
/// Will resize the image in memory to have a certain width using [ResizeImage] /// 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] /// 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. /// 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. /// 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 /// CachedNetworkImage shows a network image using a caching mechanism. It also
/// provides support for a placeholder, showing an error and fading into the /// 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 /// loaded image. Next to that it supports most features of a default Image
/// widget. /// widget.
CachedNetworkImage({ CachedNetworkImage({
Key key, Key? key,
@required this.imageUrl, required this.imageUrl,
this.httpHeaders, this.httpHeaders,
this.imageBuilder, this.imageBuilder,
this.placeholder, this.placeholder,
@ -204,17 +204,8 @@ class CachedNetworkImage extends StatelessWidget {
this.cacheKey, this.cacheKey,
this.maxWidthDiskCache, this.maxWidthDiskCache,
this.maxHeightDiskCache, this.maxHeightDiskCache,
ImageRenderMethodForWeb imageRenderMethodForWeb, ImageRenderMethodForWeb? imageRenderMethodForWeb,
}) : assert(imageUrl != null), }) : _image = CachedNetworkImageProvider(
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(
imageUrl, imageUrl,
headers: httpHeaders, headers: httpHeaders,
cacheManager: cacheManager, cacheManager: cacheManager,
@ -267,32 +258,32 @@ class CachedNetworkImage extends StatelessWidget {
} }
Widget _octoImageBuilder(BuildContext context, Widget child) { Widget _octoImageBuilder(BuildContext context, Widget child) {
return imageBuilder(context, child, _image); return imageBuilder!(context, child, _image);
} }
Widget _octoPlaceholderBuilder(BuildContext context) { Widget _octoPlaceholderBuilder(BuildContext context) {
return placeholder(context, imageUrl); return placeholder!(context, imageUrl);
} }
Widget _octoProgressIndicatorBuilder( Widget _octoProgressIndicatorBuilder(
BuildContext context, BuildContext context,
ImageChunkEvent progress, ImageChunkEvent? progress,
) { ) {
int totalSize; int? totalSize;
var downloaded = 0; var downloaded = 0;
if (progress != null) { if (progress != null) {
totalSize = progress.expectedTotalBytes; totalSize = progress.expectedTotalBytes;
downloaded = progress.cumulativeBytesLoaded; downloaded = progress.cumulativeBytesLoaded;
} }
return progressIndicatorBuilder( return progressIndicatorBuilder!(
context, imageUrl, DownloadProgress(imageUrl, totalSize, downloaded)); context, imageUrl, DownloadProgress(imageUrl, totalSize, downloaded));
} }
Widget _octoErrorBuilder( Widget _octoErrorBuilder(
BuildContext context, BuildContext context,
Object error, Object error,
StackTrace stackTrace, StackTrace? stackTrace,
) { ) {
return errorWidget(context, imageUrl, error); return errorWidget!(context, imageUrl, error);
} }
} }

View file

@ -25,12 +25,16 @@ class ConnectArguments {
class Connect extends StatefulWidget { class Connect extends StatefulWidget {
static const routeName = "/connect"; static const routeName = "/connect";
static Route buildRoute(ConnectArguments args) => MaterialPageRoute<Account>(
builder: (context) => Connect.fromArgs(args),
);
Connect({ Connect({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
Connect.fromArgs(ConnectArguments args, {Key key}) Connect.fromArgs(ConnectArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -82,7 +86,7 @@ class _ConnectState extends State<Connect> {
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
Text( Text(
AppLocalizations.of(context) AppLocalizations.of(context)!
.connectingToServer(widget.account.url), .connectingToServer(widget.account.url),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline6, style: Theme.of(context).textTheme.headline6,
@ -106,7 +110,7 @@ class _ConnectState extends State<Connect> {
} else if (state.exception is ApiException && } else if (state.exception is ApiException &&
(state.exception as ApiException).response.statusCode == 401) { (state.exception as ApiException).response.statusCode == 401) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).errorWrongPassword), content: Text(AppLocalizations.of(context)!.errorWrongPassword),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
Navigator.of(context).pop(null); Navigator.of(context).pop(null);
@ -124,9 +128,9 @@ class _ConnectState extends State<Connect> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).serverCertErrorDialogTitle), title: Text(AppLocalizations.of(context)!.serverCertErrorDialogTitle),
content: content:
Text(AppLocalizations.of(context).serverCertErrorDialogContent), Text(AppLocalizations.of(context)!.serverCertErrorDialogContent),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -138,7 +142,7 @@ class _ConnectState extends State<Connect> {
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
}, },
child: Text(AppLocalizations.of(context).advancedButtonLabel), child: Text(AppLocalizations.of(context)!.advancedButtonLabel),
), ),
], ],
), ),
@ -150,8 +154,9 @@ class _ConnectState extends State<Connect> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).whitelistCertDialogTitle), title: Text(AppLocalizations.of(context)!.whitelistCertDialogTitle),
content: Text(AppLocalizations.of(context).whitelistCertDialogContent( content: Text(AppLocalizations.of(context)!
.whitelistCertDialogContent(
SelfSignedCertManager().getLastBadCertHost(), SelfSignedCertManager().getLastBadCertHost(),
SelfSignedCertManager().getLastBadCertFingerprint())), SelfSignedCertManager().getLastBadCertFingerprint())),
actions: <Widget>[ actions: <Widget>[
@ -166,7 +171,7 @@ class _ConnectState extends State<Connect> {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
}, },
child: child:
Text(AppLocalizations.of(context).whitelistCertButtonLabel), Text(AppLocalizations.of(context)!.whitelistCertButtonLabel),
), ),
], ],
), ),

View file

@ -100,7 +100,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
dense: true, dense: true,
leading: const SizedBox(width: 24), leading: const SizedBox(width: 24),
title: Text( title: Text(
AppLocalizations.of(context).rootPickerNavigateUpItemText), AppLocalizations.of(context)!.rootPickerNavigateUpItemText),
onTap: () { onTap: () {
try { try {
_navigateInto(File(path: path.dirname(_currentPath))); _navigateInto(File(path: path.dirname(_currentPath)));
@ -128,7 +128,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
final canPick = canPickDir(item.file); final canPick = canPickDir(item.file);
final pickState = _isItemPicked(item); final pickState = _isItemPicked(item);
IconData iconData; IconData? iconData;
if (canPick) { if (canPick) {
switch (pickState) { switch (pickState) {
case _PickState.picked: case _PickState.picked:
@ -170,9 +170,10 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
onPressed: null, onPressed: null,
), ),
title: Text(path.basename(item.file.path)), title: Text(path.basename(item.file.path)),
trailing: trailing: item.children?.isNotEmpty == true
item.children.isNotEmpty ? const Icon(Icons.arrow_forward_ios) : null, ? const Icon(Icons.arrow_forward_ios)
onTap: item.children.isNotEmpty : null,
onTap: item.children?.isNotEmpty == true
? () { ? () {
try { try {
_navigateInto(item.file); _navigateInto(item.file);
@ -238,12 +239,12 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
// this dir is explicitly picked, nothing more to do // this dir is explicitly picked, nothing more to do
return [item.file]; return [item.file];
} }
if (item.children == null || item.children.isEmpty) { if (item.children == null || item.children!.isEmpty) {
return []; return [];
} }
final products = <File>[]; final products = <File>[];
for (final i in item.children) { for (final i in item.children!) {
products.addAll(_optimizePicks(i)); products.addAll(_optimizePicks(i));
} }
// // see if all children are being picked // // 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)); _picks.removeWhere((element) => identical(element, parent));
} catch (_) { } catch (_) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.rootPickerUnpickFailureNotification))); .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 /// Either [path] or [item] must be set, If both are set, [item] takes
/// priority /// priority
List<LsDirBlocItem> _pickedAllExclude({ List<LsDirBlocItem> _pickedAllExclude({
String path, String? path,
LsDirBlocItem item, LsDirBlocItem? item,
@required LsDirBlocItem exclude, required LsDirBlocItem exclude,
}) { }) {
assert(path != null || item != null);
if (item == null) { if (item == null) {
final item = _findChildItemByPath(_root, path); final item = _findChildItemByPath(_root, path!);
return _pickedAllExclude(item: item, exclude: exclude); return _pickedAllExclude(item: item, exclude: exclude);
} }
@ -311,7 +313,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
_log.fine( _log.fine(
"[_pickedAllExclude] Unpicking '${item.file.path}' and picking children"); "[_pickedAllExclude] Unpicking '${item.file.path}' and picking children");
final products = <LsDirBlocItem>[]; final products = <LsDirBlocItem>[];
for (final i in item.children) { for (final i in item.children ?? []) {
if (exclude.file.path.startsWith(i.file.path)) { if (exclude.file.path.startsWith(i.file.path)) {
// [i] is a parent of exclude // [i] is a parent of exclude
products.addAll(_pickedAllExclude(item: i, exclude: 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) { if (path == parent.file.path) {
return parent; return parent;
} }
for (final c in parent.children) { for (final c in parent.children ?? []) {
if (path == c.file.path || path.startsWith("${c.file.path}/")) { if (path == c.file.path || path.startsWith("${c.file.path}/")) {
return _findChildItemByPath(c, path); return _findChildItemByPath(c, path);
} }
@ -364,11 +366,11 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
_bloc.add(LsDirBlocQuery(getAccount(), file, depth: 2)); _bloc.add(LsDirBlocQuery(getAccount(), file, depth: 2));
} }
LsDirBloc _bloc; late LsDirBloc _bloc;
LsDirBlocItem _root; late LsDirBlocItem _root;
/// Track where the user is navigating in [_backingFiles] /// Track where the user is navigating in [_backingFiles]
String _currentPath; late String _currentPath;
var _picks = <File>[]; var _picks = <File>[];
static final _log = Logger("widget.dir_picker_mixin.DirPickerMixin"); static final _log = Logger("widget.dir_picker_mixin.DirPickerMixin");

View file

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
class Draggable<T> extends StatelessWidget { class Draggable<T extends Object> extends StatelessWidget {
Draggable({ Draggable({
Key key, Key? key,
@required this.data, required this.data,
@required this.child, required this.child,
this.feedback, this.feedback,
this.onDropBefore, this.onDropBefore,
this.onDropAfter, this.onDropAfter,
@ -79,7 +79,7 @@ class Draggable<T> extends StatelessWidget {
}, },
onAccept: (item) { onAccept: (item) {
_log.fine("[build] Dropping $item before $data"); _log.fine("[build] Dropping $item before $data");
onDropBefore(item); onDropBefore!(item);
}, },
), ),
), ),
@ -92,7 +92,7 @@ class Draggable<T> extends StatelessWidget {
}, },
onAccept: (item) { onAccept: (item) {
_log.fine("[build] Dropping $item after $data"); _log.fine("[build] Dropping $item after $data");
onDropAfter(item); onDropAfter!(item);
}, },
), ),
), ),
@ -105,26 +105,26 @@ class Draggable<T> extends StatelessWidget {
final T data; final T data;
final Widget child; final Widget child;
final Widget feedback; final Widget? feedback;
/// Called when some item dropped before this item /// Called when some item dropped before this item
final DragTargetAccept<T> onDropBefore; final DragTargetAccept<T>? onDropBefore;
/// Called when some item dropped after this item /// 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 /// Called when either one of onDragEnd, onDragCompleted or
/// onDraggableCanceled is called. /// onDraggableCanceled is called.
/// ///
/// The callback might be called multiple times per each drag event /// 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. /// Size of the feedback widget that appears under the pointer.
/// ///
/// Right now a translucent version of [child] is being shown /// Right now a translucent version of [child] is being shown
final Size feedbackSize; final Size? feedbackSize;
static final _log = Logger("widget.draggable.Draggable"); static final _log = Logger("widget.draggable.Draggable");
} }

View file

@ -9,21 +9,21 @@ abstract class DraggableItem {
/// The widget to show under the pointer when a drag is under way. /// 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 /// 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; bool get isDraggable => false;
DragTargetAccept<DraggableItem> get onDropBefore => null; DragTargetAccept<DraggableItem>? get onDropBefore => null;
DragTargetAccept<DraggableItem> get onDropAfter => null; DragTargetAccept<DraggableItem>? get onDropAfter => null;
VoidCallback get onDragStarted => null; VoidCallback? get onDragStarted => null;
VoidCallback get onDragEndedAny => null; VoidCallback? get onDragEndedAny => null;
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1); StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
} }
mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> { mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
@protected @protected
Widget buildDraggableItemList({ Widget buildDraggableItemList({
@required double maxCrossAxisExtent, required double maxCrossAxisExtent,
ValueChanged<double> onMaxExtentChanged, ValueChanged<double?>? onMaxExtentChanged,
}) { }) {
_maxCrossAxisExtent = maxCrossAxisExtent; _maxCrossAxisExtent = maxCrossAxisExtent;
return MeasurableItemList( return MeasurableItemList(
@ -50,12 +50,10 @@ mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
onDropAfter: item.onDropAfter, onDropAfter: item.onDropAfter,
onDragStarted: item.onDragStarted, onDragStarted: item.onDragStarted,
onDragEndedAny: item.onDragEndedAny, onDragEndedAny: item.onDragEndedAny,
feedbackSize: _maxCrossAxisExtent != null feedbackSize: Size(_maxCrossAxisExtent * .65, _maxCrossAxisExtent * .65),
? Size(_maxCrossAxisExtent * .65, _maxCrossAxisExtent * .65)
: null,
); );
} }
var _items = <DraggableItem>[]; var _items = <DraggableItem>[];
double _maxCrossAxisExtent; late double _maxCrossAxisExtent;
} }

View file

@ -43,13 +43,18 @@ class DynamicAlbumViewerArguments {
class DynamicAlbumViewer extends StatefulWidget { class DynamicAlbumViewer extends StatefulWidget {
static const routeName = "/dynamic-album-viewer"; static const routeName = "/dynamic-album-viewer";
static Route buildRoute(DynamicAlbumViewerArguments args) =>
MaterialPageRoute(
builder: (context) => DynamicAlbumViewer.fromArgs(args),
);
DynamicAlbumViewer({ DynamicAlbumViewer({
Key key, Key? key,
@required this.account, required this.account,
@required this.album, required this.album,
}) : super(key: key); }) : super(key: key);
DynamicAlbumViewer.fromArgs(DynamicAlbumViewerArguments args, {Key key}) DynamicAlbumViewer.fromArgs(DynamicAlbumViewerArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -96,19 +101,19 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
@override @override
enterEditMode() { enterEditMode() {
super.enterEditMode(); super.enterEditMode();
_editAlbum = _album.copyWith(); _editAlbum = _album!.copyWith();
} }
@override @override
validateEditMode() => _editFormKey?.currentState?.validate() == true; validateEditMode() => _editFormKey.currentState?.validate() == true;
@override @override
doneEditMode() { doneEditMode() {
try { try {
// persist the changes // persist the changes
_editFormKey.currentState.save(); _editFormKey.currentState!.save();
final newAlbum = makeEdited(_editAlbum); final newAlbum = makeEdited(_editAlbum!);
if (newAlbum.copyWith(lastUpdated: OrNull(_album.lastUpdated)) != if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
_album) { _album) {
_log.info("[doneEditMode] Album modified: $newAlbum"); _log.info("[doneEditMode] Album modified: $newAlbum");
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());
@ -142,14 +147,14 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
setState(() { setState(() {
_album = widget.album; _album = widget.album;
_transformItems(items); _transformItems(items);
final coverFile = initCover(widget.account, _backingFiles); initCover(widget.account, _backingFiles);
_updateAlbumPostPopulate(coverFile, items); _updateAlbumPostPopulate(items);
}); });
} }
}); });
} }
void _updateAlbumPostPopulate(File coverFile, List<AlbumItem> items) { void _updateAlbumPostPopulate(List<AlbumItem> items) {
List<File> timeDescSortedFiles; List<File> timeDescSortedFiles;
if (widget.album.sortProvider is AlbumTimeSortProvider) { if (widget.album.sortProvider is AlbumTimeSortProvider) {
if ((widget.album.sortProvider as AlbumTimeSortProvider).isAscending) { if ((widget.album.sortProvider as AlbumTimeSortProvider).isAscending) {
@ -168,7 +173,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
bool shouldUpdate = false; bool shouldUpdate = false;
final albumUpdatedCover = UpdateDynamicAlbumCover() final albumUpdatedCover = UpdateDynamicAlbumCover()
.updateWithSortedFiles(_album, timeDescSortedFiles); .updateWithSortedFiles(_album!, timeDescSortedFiles);
if (!identical(albumUpdatedCover, _album)) { if (!identical(albumUpdatedCover, _album)) {
_log.info("[_updateAlbumPostPopulate] Update album cover"); _log.info("[_updateAlbumPostPopulate] Update album cover");
shouldUpdate = true; shouldUpdate = true;
@ -176,7 +181,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
_album = albumUpdatedCover; _album = albumUpdatedCover;
final albumUpdatedTime = UpdateDynamicAlbumTime() final albumUpdatedTime = UpdateDynamicAlbumTime()
.updateWithSortedFiles(_album, timeDescSortedFiles); .updateWithSortedFiles(_album!, timeDescSortedFiles);
if (!identical(albumUpdatedTime, _album)) { if (!identical(albumUpdatedTime, _album)) {
_log.info( _log.info(
"[_updateAlbumPostPopulate] Update album time: ${albumUpdatedTime.provider.latestItemTime}"); "[_updateAlbumPostPopulate] Update album time: ${albumUpdatedTime.provider.latestItemTime}");
@ -185,7 +190,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
_album = albumUpdatedTime; _album = albumUpdatedTime;
if (shouldUpdate) { 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( return buildNormalAppBar(
context, context,
widget.account, widget.account,
_album, _album!,
menuItemBuilder: (context) => [ menuItemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: _menuValueConvertBasic, value: _menuValueConvertBasic,
child: Text(AppLocalizations.of(context).convertBasicAlbumMenuLabel), child: Text(AppLocalizations.of(context)!.convertBasicAlbumMenuLabel),
), ),
], ],
onSelectedMenuItem: (option) { onSelectedMenuItem: (option) {
@ -267,7 +272,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
if (platform_k.isAndroid) if (platform_k.isAndroid)
IconButton( IconButton(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
tooltip: AppLocalizations.of(context).shareSelectedTooltip, tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
onPressed: () { onPressed: () {
_onSelectionAppBarSharePressed(context); _onSelectionAppBarSharePressed(context);
}, },
@ -277,7 +282,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: _SelectionAppBarOption.delete, value: _SelectionAppBarOption.delete,
child: Text(AppLocalizations.of(context).deleteSelectedTooltip), child: Text(AppLocalizations.of(context)!.deleteSelectedTooltip),
), ),
], ],
onSelected: (option) { onSelected: (option) {
@ -293,7 +298,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
return buildEditAppBar(context, widget.account, widget.album, actions: [ return buildEditAppBar(context, widget.account, widget.album, actions: [
IconButton( IconButton(
icon: Icon(Icons.sort_by_alpha), icon: Icon(Icons.sort_by_alpha),
tooltip: AppLocalizations.of(context).sortTooltip, tooltip: AppLocalizations.of(context)!.sortTooltip,
onPressed: _onEditAppBarSortPressed, onPressed: _onEditAppBarSortPressed,
), ),
]); ]);
@ -317,9 +322,9 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context) title: Text(AppLocalizations.of(context)!
.convertBasicAlbumConfirmationDialogTitle), .convertBasicAlbumConfirmationDialogTitle),
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.convertBasicAlbumConfirmationDialogContent), .convertBasicAlbumConfirmationDialogContent),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
@ -341,17 +346,17 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
return; return;
} }
_log.info( _log.info(
"[_onAppBarConvertBasicPressed] Converting album '${_album.name}' to static"); "[_onAppBarConvertBasicPressed] Converting album '${_album!.name}' to static");
final albumRepo = AlbumRepo(AlbumCachedDataSource()); final albumRepo = AlbumRepo(AlbumCachedDataSource());
UpdateAlbum(albumRepo)( UpdateAlbum(albumRepo)(
widget.account, widget.account,
_album.copyWith( _album!.copyWith(
provider: AlbumStaticProvider(items: _sortedItems), provider: AlbumStaticProvider(items: _sortedItems),
coverProvider: AlbumAutoCoverProvider(), coverProvider: AlbumAutoCoverProvider(),
), ),
).then((value) { ).then((value) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.convertBasicAlbumSuccessNotification), .convertBasicAlbumSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -386,7 +391,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
void _onSelectionAppBarDeletePressed() async { void _onSelectionAppBarDeletePressed() async {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.deleteSelectedProcessingNotification(selectedListItems.length)), .deleteSelectedProcessingNotification(selectedListItems.length)),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
@ -417,12 +422,12 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
if (failures.isEmpty) { if (failures.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).deleteSelectedSuccessNotification), AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.deleteSelectedFailureNotification(failures.length)), .deleteSelectedFailureNotification(failures.length)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -439,14 +444,14 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
} }
void _onEditAppBarSortPressed() { void _onEditAppBarSortPressed() {
final sortProvider = _editAlbum.sortProvider; final sortProvider = _editAlbum!.sortProvider;
showDialog( showDialog(
context: context, context: context,
builder: (context) => FancyOptionPicker( builder: (context) => FancyOptionPicker(
title: AppLocalizations.of(context).sortOptionDialogTitle, title: AppLocalizations.of(context)!.sortOptionDialogTitle,
items: [ items: [
FancyOptionPickerItem( FancyOptionPickerItem(
label: AppLocalizations.of(context).sortOptionTimeAscendingLabel, label: AppLocalizations.of(context)!.sortOptionTimeAscendingLabel,
isSelected: sortProvider is AlbumTimeSortProvider && isSelected: sortProvider is AlbumTimeSortProvider &&
sortProvider.isAscending, sortProvider.isAscending,
onSelect: () { onSelect: () {
@ -455,7 +460,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
}, },
), ),
FancyOptionPickerItem( FancyOptionPickerItem(
label: AppLocalizations.of(context).sortOptionTimeDescendingLabel, label: AppLocalizations.of(context)!.sortOptionTimeDescendingLabel,
isSelected: sortProvider is AlbumTimeSortProvider && isSelected: sortProvider is AlbumTimeSortProvider &&
!sortProvider.isAscending, !sortProvider.isAscending,
onSelect: () { onSelect: () {
@ -469,7 +474,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
} }
void _onSortOldestPressed() { void _onSortOldestPressed() {
_editAlbum = _editAlbum.copyWith( _editAlbum = _editAlbum!.copyWith(
sortProvider: AlbumTimeSortProvider(isAscending: true), sortProvider: AlbumTimeSortProvider(isAscending: true),
); );
setState(() { setState(() {
@ -478,7 +483,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
} }
void _onSortNewestPressed() { void _onSortNewestPressed() {
_editAlbum = _editAlbum.copyWith( _editAlbum = _editAlbum!.copyWith(
sortProvider: AlbumTimeSortProvider(isAscending: false), sortProvider: AlbumTimeSortProvider(isAscending: false),
); );
setState(() { setState(() {
@ -489,9 +494,9 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
void _transformItems(List<AlbumItem> items) { void _transformItems(List<AlbumItem> items) {
if (_editAlbum != null) { if (_editAlbum != null) {
// edit mode // edit mode
_sortedItems = _editAlbum.sortProvider.sort(items); _sortedItems = _editAlbum!.sortProvider.sort(items);
} else { } else {
_sortedItems = _album.sortProvider.sort(items); _sortedItems = _album!.sortProvider.sort(items);
} }
_onSortedItemsUpdated(); _onSortedItemsUpdated();
} }
@ -535,12 +540,12 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
.toList(); .toList();
} }
Album _album; Album? _album;
var _sortedItems = <AlbumItem>[]; var _sortedItems = <AlbumItem>[];
var _backingFiles = <File>[]; var _backingFiles = <File>[];
final _editFormKey = GlobalKey<FormState>(); final _editFormKey = GlobalKey<FormState>();
Album _editAlbum; Album? _editAlbum;
static final _log = static final _log =
Logger("widget.dynamic_album_viewer._DynamicAlbumViewerState"); Logger("widget.dynamic_album_viewer._DynamicAlbumViewerState");
@ -549,8 +554,8 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
abstract class _ListItem implements SelectableItem { abstract class _ListItem implements SelectableItem {
_ListItem({ _ListItem({
@required this.index, required this.index,
VoidCallback onTap, VoidCallback? onTap,
}) : _onTap = onTap; }) : _onTap = onTap;
@override @override
@ -571,14 +576,14 @@ abstract class _ListItem implements SelectableItem {
final int index; final int index;
final VoidCallback _onTap; final VoidCallback? _onTap;
} }
abstract class _FileListItem extends _ListItem { abstract class _FileListItem extends _ListItem {
_FileListItem({ _FileListItem({
@required int index, required int index,
@required this.file, required this.file,
VoidCallback onTap, VoidCallback? onTap,
}) : super( }) : super(
index: index, index: index,
onTap: onTap, onTap: onTap,
@ -589,11 +594,11 @@ abstract class _FileListItem extends _ListItem {
class _ImageListItem extends _FileListItem { class _ImageListItem extends _FileListItem {
_ImageListItem({ _ImageListItem({
@required int index, required int index,
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
}) : super( }) : super(
index: index, index: index,
file: file, file: file,
@ -615,11 +620,11 @@ class _ImageListItem extends _FileListItem {
class _VideoListItem extends _FileListItem { class _VideoListItem extends _FileListItem {
_VideoListItem({ _VideoListItem({
@required int index, required int index,
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
}) : super( }) : super(
index: index, index: index,
file: file, file: file,

View file

@ -3,28 +3,28 @@ import 'package:flutter/widgets.dart';
class FancyOptionPickerItem { class FancyOptionPickerItem {
FancyOptionPickerItem({ FancyOptionPickerItem({
@required this.label, required this.label,
this.isSelected = false, this.isSelected = false,
this.onSelect, this.onSelect,
}); });
String label; String label;
bool isSelected; bool isSelected;
VoidCallback onSelect; VoidCallback? onSelect;
} }
/// A fancy looking dialog to pick an option /// A fancy looking dialog to pick an option
class FancyOptionPicker extends StatelessWidget { class FancyOptionPicker extends StatelessWidget {
FancyOptionPicker({ FancyOptionPicker({
Key key, Key? key,
this.title, this.title,
@required this.items, required this.items,
}) : super(key: key); }) : super(key: key);
@override @override
build(BuildContext context) { build(BuildContext context) {
return SimpleDialog( return SimpleDialog(
title: title != null ? Text(title) : null, title: title != null ? Text(title!) : null,
children: items children: items
.map((e) => SimpleDialogOption( .map((e) => SimpleDialogOption(
child: ListTile( child: ListTile(
@ -47,6 +47,6 @@ class FancyOptionPicker extends StatelessWidget {
); );
} }
final String title; final String? title;
final List<FancyOptionPickerItem> items; final List<FancyOptionPickerItem> items;
} }

View file

@ -16,12 +16,16 @@ class HomeArguments {
class Home extends StatefulWidget { class Home extends StatefulWidget {
static const routeName = "/home"; static const routeName = "/home";
static Route buildRoute(HomeArguments args) => MaterialPageRoute(
builder: (context) => Home.fromArgs(args),
);
Home({ Home({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
Home.fromArgs(HomeArguments args, {Key key}) Home.fromArgs(HomeArguments args, {Key? key})
: this( : this(
account: args.account, account: args.account,
); );
@ -33,12 +37,6 @@ class Home extends StatefulWidget {
} }
class _HomeState extends State<Home> { class _HomeState extends State<Home> {
@override
initState() {
super.initState();
_pageController = PageController(initialPage: 0, keepPage: false);
}
@override @override
build(BuildContext context) { build(BuildContext context) {
return AppTheme( return AppTheme(
@ -54,11 +52,11 @@ class _HomeState extends State<Home> {
items: <BottomNavigationBarItem>[ items: <BottomNavigationBarItem>[
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.photo_outlined), icon: const Icon(Icons.photo_outlined),
label: AppLocalizations.of(context).photosTabLabel, label: AppLocalizations.of(context)!.photosTabLabel,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.photo_album_outlined), icon: const Icon(Icons.photo_album_outlined),
label: AppLocalizations.of(context).albumsTabLabel, label: AppLocalizations.of(context)!.albumsTabLabel,
), ),
], ],
currentIndex: _nextPage, currentIndex: _nextPage,
@ -108,6 +106,6 @@ class _HomeState extends State<Home> {
}); });
} }
PageController _pageController; final _pageController = PageController(initialPage: 0, keepPage: false);
int _nextPage = 0; int _nextPage = 0;
} }

View file

@ -32,8 +32,8 @@ import 'package:tuple/tuple.dart';
class HomeAlbums extends StatefulWidget { class HomeAlbums extends StatefulWidget {
HomeAlbums({ HomeAlbums({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
@override @override
@ -83,7 +83,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
_reqQuery(); _reqQuery();
} else { } else {
// process the current state // process the current state
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
setState(() { setState(() {
_onStateChange(context, _bloc.state); _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)), .selectionAppBarTitle(_selectedItems.length)),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
tooltip: AppLocalizations.of(context).deleteSelectedTooltip, tooltip: AppLocalizations.of(context)!.deleteSelectedTooltip,
onPressed: () { onPressed: () {
_onSelectionAppBarDeletePressed(); _onSelectionAppBarDeletePressed();
}, },
@ -171,13 +171,13 @@ class _HomeAlbumsState extends State<HomeAlbums>
IconButton( IconButton(
onPressed: () => _onSearchPressed(context), onPressed: () => _onSearchPressed(context),
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
tooltip: AppLocalizations.of(context).searchTooltip, tooltip: AppLocalizations.of(context)!.searchTooltip,
), ),
], ],
menuActions: [ menuActions: [
PopupMenuItem( PopupMenuItem(
value: _menuValueImport, value: _menuValueImport,
child: Text(AppLocalizations.of(context).importFoldersTooltip), child: Text(AppLocalizations.of(context)!.importFoldersTooltip),
), ),
], ],
onSelectedMenuActions: (option) { onSelectedMenuActions: (option) {
@ -225,7 +225,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
), ),
), ),
), ),
title: AppLocalizations.of(context).albumArchiveLabel, title: AppLocalizations.of(context)!.albumArchiveLabel,
onTap: () { onTap: () {
Navigator.of(context).pushNamed(ArchiveViewer.routeName, Navigator.of(context).pushNamed(ArchiveViewer.routeName,
arguments: ArchiveViewerArguments(widget.account)); 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), onTap: () => _onNewAlbumItemTap(context),
); );
} }
@ -324,7 +324,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
"[_onNewAlbumItemTap] Failed while showDialog", e, stacktrace); "[_onNewAlbumItemTap] Failed while showDialog", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: content:
Text(AppLocalizations.of(context).createAlbumFailureNotification), Text(AppLocalizations.of(context)!.createAlbumFailureNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
}); });
@ -337,11 +337,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
Future<void> _onSelectionAppBarDeletePressed() async { Future<void> _onSelectionAppBarDeletePressed() async {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.deleteSelectedProcessingNotification(_selectedItems.length)), .deleteSelectedProcessingNotification(_selectedItems.length)),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
final selectedFiles = _selectedItems.map((e) => e.album.albumFile).toList(); final selectedFiles =
_selectedItems.map((e) => e.album.albumFile!).toList();
setState(() { setState(() {
_selectedItems.clear(); _selectedItems.clear();
}); });
@ -363,12 +364,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
if (failures.isEmpty) { if (failures.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).deleteSelectedSuccessNotification), AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.deleteSelectedFailureNotification(failures.length)), .deleteSelectedFailureNotification(failures.length)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -411,12 +412,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
.map((from) { .map((from) {
try { try {
return _items.whereType<_GridItem>().firstWhere( return _items.whereType<_GridItem>().firstWhere(
(to) => from.album.albumFile.path == to.album.albumFile.path); (to) => from.album.albumFile!.path == to.album.albumFile!.path);
} catch (_) { } catch (_) {
return null; return null;
} }
}) })
.where((element) => element != null) .whereType<_GridItem>()
.toList(); .toList();
_selectedItems _selectedItems
..clear() ..clear()
@ -439,7 +440,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
bool get _isSelectionMode => _selectedItems.isNotEmpty; bool get _isSelectionMode => _selectedItems.isNotEmpty;
ListAlbumBloc _bloc; late ListAlbumBloc _bloc;
final _items = <_GridItem>[]; final _items = <_GridItem>[];
final _selectedItems = <_GridItem>[]; final _selectedItems = <_GridItem>[];

View file

@ -13,8 +13,8 @@ import 'package:nc_photos/widget/settings.dart';
/// AppBar for home screens /// AppBar for home screens
class HomeSliverAppBar extends StatelessWidget { class HomeSliverAppBar extends StatelessWidget {
HomeSliverAppBar({ HomeSliverAppBar({
Key key, Key? key,
@required this.account, required this.account,
this.actions, this.actions,
this.menuActions, this.menuActions,
this.onSelectedMenuActions, this.onSelectedMenuActions,
@ -90,7 +90,7 @@ class HomeSliverAppBar extends StatelessWidget {
inactiveThumbImage: inactiveThumbImage:
const AssetImage("assets/ic_dark_mode_switch_24dp.png"), const AssetImage("assets/ic_dark_mode_switch_24dp.png"),
), ),
PopupMenuButton( PopupMenuButton<int>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip, tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) => itemBuilder: (context) =>
(menuActions ?? []) + (menuActions ?? []) +
@ -98,7 +98,7 @@ class HomeSliverAppBar extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: _menuValueAbout, value: _menuValueAbout,
child: child:
Text(AppLocalizations.of(context).settingsMenuLabel), Text(AppLocalizations.of(context)!.settingsMenuLabel),
), ),
], ],
onSelected: (option) { onSelected: (option) {
@ -125,12 +125,12 @@ class HomeSliverAppBar extends StatelessWidget {
final Account account; final Account account;
/// Screen specific action buttons /// Screen specific action buttons
final List<Widget> actions; final List<Widget>? actions;
/// Screen specific actions under the overflow menu. The value of each item /// Screen specific actions under the overflow menu. The value of each item
/// much >= 0 /// much >= 0
final List<PopupMenuEntry<int>> menuActions; final List<PopupMenuEntry<int>>? menuActions;
final void Function(int) onSelectedMenuActions; final void Function(int)? onSelectedMenuActions;
static const _menuValueAbout = -1; static const _menuValueAbout = -1;
} }

View file

@ -41,8 +41,8 @@ import 'package:nc_photos/widget/viewer.dart';
class HomePhotos extends StatefulWidget { class HomePhotos extends StatefulWidget {
HomePhotos({ HomePhotos({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
@override @override
@ -59,7 +59,7 @@ class _HomePhotosState extends State<HomePhotos>
@override @override
initState() { initState() {
super.initState(); super.initState();
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevel(0); _thumbZoomLevel = Pref.inst().getHomePhotosZoomLevelOr(0);
_initBloc(); _initBloc();
} }
@ -82,7 +82,7 @@ class _HomePhotosState extends State<HomePhotos>
_reqQuery(); _reqQuery();
} else { } else {
// process the current state // process the current state
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
setState(() { setState(() {
_onStateChange(context, _bloc.state); _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)), .selectionAppBarTitle(selectedListItems.length)),
actions: [ actions: [
if (platform_k.isAndroid) if (platform_k.isAndroid)
IconButton( IconButton(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
tooltip: AppLocalizations.of(context).shareSelectedTooltip, tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
onPressed: () { onPressed: () {
_onSelectionAppBarSharePressed(context); _onSelectionAppBarSharePressed(context);
}, },
), ),
IconButton( IconButton(
icon: const Icon(Icons.playlist_add), icon: const Icon(Icons.playlist_add),
tooltip: AppLocalizations.of(context).addSelectedToAlbumTooltip, tooltip: AppLocalizations.of(context)!.addSelectedToAlbumTooltip,
onPressed: () { onPressed: () {
_onSelectionAppBarAddToAlbumPressed(context); _onSelectionAppBarAddToAlbumPressed(context);
}, },
), ),
PopupMenuButton( PopupMenuButton<_SelectionAppBarMenuOption>(
tooltip: MaterialLocalizations.of(context).moreButtonTooltip, tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: _SelectionAppBarMenuOption.archive, value: _SelectionAppBarMenuOption.archive,
child: child: Text(
Text(AppLocalizations.of(context).archiveSelectedMenuLabel), AppLocalizations.of(context)!.archiveSelectedMenuLabel),
), ),
PopupMenuItem( PopupMenuItem(
value: _SelectionAppBarMenuOption.delete, value: _SelectionAppBarMenuOption.delete,
child: Text(AppLocalizations.of(context).deleteSelectedTooltip), child:
Text(AppLocalizations.of(context)!.deleteSelectedTooltip),
), ),
], ],
onSelected: (option) { onSelected: (option) {
@ -212,7 +213,7 @@ class _HomePhotosState extends State<HomePhotos>
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
icon: const Icon(Icons.photo_size_select_large), icon: const Icon(Icons.photo_size_select_large),
tooltip: AppLocalizations.of(context).zoomTooltip, tooltip: AppLocalizations.of(context)!.zoomTooltip,
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuZoom( PopupMenuZoom(
initialValue: _thumbZoomLevel, initialValue: _thumbZoomLevel,
@ -231,7 +232,7 @@ class _HomePhotosState extends State<HomePhotos>
menuActions: [ menuActions: [
PopupMenuItem( PopupMenuItem(
value: _menuValueRefresh, value: _menuValueRefresh,
child: Text(AppLocalizations.of(context).refreshMenuLabel), child: Text(AppLocalizations.of(context)!.refreshMenuLabel),
), ),
], ],
onSelectedMenuActions: (option) { onSelectedMenuActions: (option) {
@ -251,7 +252,7 @@ class _HomePhotosState extends State<HomePhotos>
} else if (state is ScanDirBlocSuccess || state is ScanDirBlocLoading) { } else if (state is ScanDirBlocSuccess || state is ScanDirBlocLoading) {
_transformItems(state.files); _transformItems(state.files);
if (state is ScanDirBlocSuccess) { if (state is ScanDirBlocSuccess) {
if (Pref.inst().isEnableExif() && !_hasFiredMetadataTask.value) { if (Pref.inst().isEnableExifOr() && !_hasFiredMetadataTask.value) {
KiwiContainer() KiwiContainer()
.resolve<MetadataTaskManager>() .resolve<MetadataTaskManager>()
.addTask(MetadataTask(widget.account)); .addTask(MetadataTask(widget.account));
@ -306,14 +307,14 @@ class _HomePhotosState extends State<HomePhotos>
clearSelectedItems(); clearSelectedItems();
}); });
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.addSelectedToAlbumSuccessNotification(value.name)), .addSelectedToAlbumSuccessNotification(value.name)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
}).catchError((_) {}); }).catchError((_) {});
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.addSelectedToAlbumFailureNotification), .addSelectedToAlbumFailureNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -325,7 +326,7 @@ class _HomePhotosState extends State<HomePhotos>
stacktrace); stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
"${AppLocalizations.of(context).addSelectedToAlbumFailureNotification}: " "${AppLocalizations.of(context)!.addSelectedToAlbumFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -355,7 +356,7 @@ class _HomePhotosState extends State<HomePhotos>
"[_addSelectedToAlbum] Failed while updating album", e, stacktrace); "[_addSelectedToAlbum] Failed while updating album", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
"${AppLocalizations.of(context).addSelectedToAlbumFailureNotification}: " "${AppLocalizations.of(context)!.addSelectedToAlbumFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -365,7 +366,7 @@ class _HomePhotosState extends State<HomePhotos>
Future<void> _onSelectionAppBarDeletePressed(BuildContext context) async { Future<void> _onSelectionAppBarDeletePressed(BuildContext context) async {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.deleteSelectedProcessingNotification(selectedListItems.length)), .deleteSelectedProcessingNotification(selectedListItems.length)),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
@ -394,12 +395,12 @@ class _HomePhotosState extends State<HomePhotos>
if (failures.isEmpty) { if (failures.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).deleteSelectedSuccessNotification), AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.deleteSelectedFailureNotification(failures.length)), .deleteSelectedFailureNotification(failures.length)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -425,7 +426,7 @@ class _HomePhotosState extends State<HomePhotos>
Future<void> _onSelectionAppBarArchivePressed(BuildContext context) async { Future<void> _onSelectionAppBarArchivePressed(BuildContext context) async {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.archiveSelectedProcessingNotification(selectedListItems.length)), .archiveSelectedProcessingNotification(selectedListItems.length)),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
@ -454,12 +455,12 @@ class _HomePhotosState extends State<HomePhotos>
if (failures.isEmpty) { if (failures.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).archiveSelectedSuccessNotification), AppLocalizations.of(context)!.archiveSelectedSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.archiveSelectedFailureNotification(failures.length)), .archiveSelectedFailureNotification(failures.length)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -478,16 +479,16 @@ class _HomePhotosState extends State<HomePhotos>
file_util.isSupportedFormat(element) && element.isArchived != true) file_util.isSupportedFormat(element) && element.isArchived != true)
.sorted(compareFileDateTimeDescending); .sorted(compareFileDateTimeDescending);
DateTime currentDate; DateTime? currentDate;
final isMonthOnly = _thumbZoomLevel < 0; final isMonthOnly = _thumbZoomLevel < 0;
itemStreamListItems = () sync* { itemStreamListItems = () sync* {
for (int i = 0; i < _backingFiles.length; ++i) { for (int i = 0; i < _backingFiles.length; ++i) {
final f = _backingFiles[i]; final f = _backingFiles[i];
final newDate = f.bestDateTime?.toLocal(); final newDate = f.bestDateTime.toLocal();
if (newDate?.year != currentDate?.year || if (newDate.year != currentDate?.year ||
newDate?.month != currentDate?.month || newDate.month != currentDate?.month ||
(!isMonthOnly && newDate?.day != currentDate?.day)) { (!isMonthOnly && newDate.day != currentDate?.day)) {
yield _DateListItem(date: newDate, isMonthOnly: isMonthOnly); yield _DateListItem(date: newDate, isMonthOnly: isMonthOnly);
currentDate = newDate; currentDate = newDate;
} }
@ -546,13 +547,13 @@ class _HomePhotosState extends State<HomePhotos>
} }
/// Return the estimated scroll extent of the custom scroll view, or null /// Return the estimated scroll extent of the custom scroll view, or null
double _getScrollViewExtent(BoxConstraints constraints) { double? _getScrollViewExtent(BoxConstraints constraints) {
if (_itemListMaxExtent != null && if (_itemListMaxExtent != null &&
constraints.hasBoundedHeight && constraints.hasBoundedHeight &&
_appBarExtent != null) { _appBarExtent != null) {
// scroll extent = list height - widget viewport height + sliver app bar height + list padding // scroll extent = list height - widget viewport height + sliver app bar height + list padding
final scrollExtent = final scrollExtent =
_itemListMaxExtent - constraints.maxHeight + _appBarExtent + 16; _itemListMaxExtent! - constraints.maxHeight + _appBarExtent! + 16;
_log.info( _log.info(
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + 16 = $scrollExtent"); "[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + 16 = $scrollExtent");
return scrollExtent; return scrollExtent;
@ -595,7 +596,7 @@ class _HomePhotosState extends State<HomePhotos>
} }
} }
ScanDirBloc _bloc; late ScanDirBloc _bloc;
var _backingFiles = <File>[]; var _backingFiles = <File>[];
@ -603,8 +604,8 @@ class _HomePhotosState extends State<HomePhotos>
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
double _appBarExtent; double? _appBarExtent;
double _itemListMaxExtent; double? _itemListMaxExtent;
static final _log = Logger("widget.home_photos._HomePhotosState"); static final _log = Logger("widget.home_photos._HomePhotosState");
static const _menuValueRefresh = 0; static const _menuValueRefresh = 0;
@ -612,7 +613,7 @@ class _HomePhotosState extends State<HomePhotos>
abstract class _ListItem implements SelectableItem { abstract class _ListItem implements SelectableItem {
_ListItem({ _ListItem({
VoidCallback onTap, VoidCallback? onTap,
}) : _onTap = onTap; }) : _onTap = onTap;
@override @override
@ -624,12 +625,12 @@ abstract class _ListItem implements SelectableItem {
@override @override
get staggeredTile => const StaggeredTile.count(1, 1); get staggeredTile => const StaggeredTile.count(1, 1);
final VoidCallback _onTap; final VoidCallback? _onTap;
} }
class _DateListItem extends _ListItem { class _DateListItem extends _ListItem {
_DateListItem({ _DateListItem({
@required this.date, required this.date,
this.isMonthOnly = false, this.isMonthOnly = false,
}); });
@ -647,7 +648,7 @@ class _DateListItem extends _ListItem {
isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY; isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY;
subtitle = subtitle =
DateFormat(pattern, Localizations.localeOf(context).languageCode) DateFormat(pattern, Localizations.localeOf(context).languageCode)
.format(date.toLocal()); .format(date!.toLocal());
} }
return Align( return Align(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
@ -655,7 +656,7 @@ class _DateListItem extends _ListItem {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text( child: Text(
subtitle, subtitle,
style: Theme.of(context).textTheme.caption.copyWith( style: Theme.of(context).textTheme.caption!.copyWith(
color: AppTheme.getPrimaryTextColor(context), color: AppTheme.getPrimaryTextColor(context),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -664,14 +665,14 @@ class _DateListItem extends _ListItem {
); );
} }
final DateTime date; final DateTime? date;
final bool isMonthOnly; final bool isMonthOnly;
} }
abstract class _FileListItem extends _ListItem { abstract class _FileListItem extends _ListItem {
_FileListItem({ _FileListItem({
@required this.file, required this.file,
VoidCallback onTap, VoidCallback? onTap,
}) : super(onTap: onTap); }) : super(onTap: onTap);
@override @override
@ -687,10 +688,10 @@ abstract class _FileListItem extends _ListItem {
class _ImageListItem extends _FileListItem { class _ImageListItem extends _FileListItem {
_ImageListItem({ _ImageListItem({
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
}) : super(file: file, onTap: onTap); }) : super(file: file, onTap: onTap);
@override @override
@ -708,10 +709,10 @@ class _ImageListItem extends _FileListItem {
class _VideoListItem extends _FileListItem { class _VideoListItem extends _FileListItem {
_VideoListItem({ _VideoListItem({
@required File file, required File file,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
VoidCallback onTap, VoidCallback? onTap,
}) : super(file: file, onTap: onTap); }) : super(file: file, onTap: onTap);
@override @override

View file

@ -11,9 +11,9 @@ import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
class ImageViewer extends StatefulWidget { class ImageViewer extends StatefulWidget {
ImageViewer({ ImageViewer({
@required this.account, required this.account,
@required this.file, required this.file,
this.canZoom, required this.canZoom,
this.onLoaded, this.onLoaded,
this.onHeightChanged, this.onHeightChanged,
this.onZoomStarted, this.onZoomStarted,
@ -35,10 +35,10 @@ class ImageViewer extends StatefulWidget {
final Account account; final Account account;
final File file; final File file;
final bool canZoom; final bool canZoom;
final VoidCallback onLoaded; final VoidCallback? onLoaded;
final void Function(double height) onHeightChanged; final ValueChanged<double>? onHeightChanged;
final VoidCallback onZoomStarted; final VoidCallback? onZoomStarted;
final VoidCallback onZoomEnded; final VoidCallback? onZoomEnded;
} }
class _ImageViewerState extends State<ImageViewer> class _ImageViewerState extends State<ImageViewer>
@ -58,9 +58,9 @@ class _ImageViewerState extends State<ImageViewer>
alignment: Alignment.center, alignment: Alignment.center,
child: NotificationListener<SizeChangedLayoutNotification>( child: NotificationListener<SizeChangedLayoutNotification>(
onNotification: (_) { onNotification: (_) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
if (_key.currentContext != null) { if (_key.currentContext != null) {
widget.onHeightChanged?.call(_key.currentContext.size.height); widget.onHeightChanged?.call(_key.currentContext!.size!.height);
} }
}); });
return false; return false;
@ -78,7 +78,7 @@ class _ImageViewerState extends State<ImageViewer>
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
imageBuilder: (context, child, imageProvider) { imageBuilder: (context, child, imageProvider) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
_onItemLoaded(); _onItemLoaded();
}); });
SizeChangedLayoutNotification().dispatch(context); SizeChangedLayoutNotification().dispatch(context);

View file

@ -10,11 +10,11 @@ abstract class MeasurableItemListState {
class MeasurableItemList extends StatefulWidget { class MeasurableItemList extends StatefulWidget {
MeasurableItemList({ MeasurableItemList({
Key key, Key? key,
@required this.maxCrossAxisExtent, required this.maxCrossAxisExtent,
@required this.itemCount, required this.itemCount,
@required this.itemBuilder, required this.itemBuilder,
@required this.staggeredTileBuilder, required this.staggeredTileBuilder,
this.onMaxExtentChanged, this.onMaxExtentChanged,
}) : super(key: key); }) : super(key: key);
@ -25,7 +25,7 @@ class MeasurableItemList extends StatefulWidget {
final int itemCount; final int itemCount;
final IndexedWidgetBuilder itemBuilder; final IndexedWidgetBuilder itemBuilder;
final IndexedStaggeredTileBuilder staggeredTileBuilder; final IndexedStaggeredTileBuilder staggeredTileBuilder;
final ValueChanged<double> onMaxExtentChanged; final ValueChanged<double?>? onMaxExtentChanged;
} }
class _MeasurableItemListState extends State<MeasurableItemList> class _MeasurableItemListState extends State<MeasurableItemList>
@ -35,21 +35,21 @@ class _MeasurableItemListState extends State<MeasurableItemList>
initState() { initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
_prevOrientation = MediaQuery.of(context).orientation; _prevOrientation = MediaQuery.of(context).orientation;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance!.addObserver(this);
}); });
} }
@override @override
dispose() { dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance!.removeObserver(this);
super.dispose(); super.dispose();
} }
@override @override
didChangeMetrics() { didChangeMetrics() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
final orientation = MediaQuery.of(context).orientation; final orientation = MediaQuery.of(context).orientation;
if (orientation != _prevOrientation) { if (orientation != _prevOrientation) {
_log.info( _log.info(
@ -70,7 +70,8 @@ class _MeasurableItemListState extends State<MeasurableItemList>
} }
if (constraints.crossAxisExtent != _prevListWidth) { if (constraints.crossAxisExtent != _prevListWidth) {
_log.info("[build] updateListHeight: list viewport width changed"); _log.info("[build] updateListHeight: list viewport width changed");
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight()); WidgetsBinding.instance!
.addPostFrameCallback((_) => updateListHeight());
_prevListWidth = constraints.crossAxisExtent; _prevListWidth = constraints.crossAxisExtent;
} }
@ -78,7 +79,8 @@ class _MeasurableItemListState extends State<MeasurableItemList>
final cellSize = widget.maxCrossAxisExtent; final cellSize = widget.maxCrossAxisExtent;
if (cellSize != _prevCellSize) { if (cellSize != _prevCellSize) {
_log.info("[build] updateListHeight: cell size changed"); _log.info("[build] updateListHeight: cell size changed");
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight()); WidgetsBinding.instance!
.addPostFrameCallback((_) => updateListHeight());
_prevCellSize = cellSize; _prevCellSize = cellSize;
} }
_gridKey = _GridKey("$_uniqueToken $cellSize"); _gridKey = _GridKey("$_uniqueToken $cellSize");
@ -94,9 +96,9 @@ class _MeasurableItemListState extends State<MeasurableItemList>
@override @override
updateListHeight() { updateListHeight() {
double newMaxExtent; double? newMaxExtent;
try { try {
final renderObj = _gridKey.currentContext.findRenderObject() final renderObj = _gridKey.currentContext!.findRenderObject()
as RenderMeasurableSliverStaggeredGrid; as RenderMeasurableSliverStaggeredGrid;
final maxExtent = renderObj.calculateExtent(); final maxExtent = renderObj.calculateExtent();
_log.info("[updateListHeight] Max extent: $maxExtent"); _log.info("[updateListHeight] Max extent: $maxExtent");
@ -118,14 +120,14 @@ class _MeasurableItemListState extends State<MeasurableItemList>
} }
} }
double _prevListWidth; double? _prevListWidth;
double _prevCellSize; double? _prevCellSize;
double _maxExtent; double? _maxExtent;
Orientation _prevOrientation; Orientation? _prevOrientation;
// this unique token is there to keep the global key unique // this unique token is there to keep the global key unique
final _uniqueToken = Uuid().v4(); final _uniqueToken = Uuid().v4();
GlobalObjectKey _gridKey; late GlobalObjectKey _gridKey;
static final _log = static final _log =
Logger("widget.measurable_item_list._MeasurableItemListState"); Logger("widget.measurable_item_list._MeasurableItemListState");

View file

@ -8,9 +8,9 @@ class MeasureSize extends SingleChildRenderObjectWidget {
final OnWidgetSizeChanged onChange; final OnWidgetSizeChanged onChange;
const MeasureSize({ const MeasureSize({
Key key, Key? key,
@required this.onChange, required this.onChange,
@required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
@override @override
@ -20,7 +20,7 @@ class MeasureSize extends SingleChildRenderObjectWidget {
} }
class _MeasureSizeRenderObject extends RenderProxyBox { class _MeasureSizeRenderObject extends RenderProxyBox {
Size oldSize; Size? oldSize;
final OnWidgetSizeChanged onChange; final OnWidgetSizeChanged onChange;
_MeasureSizeRenderObject(this.onChange); _MeasureSizeRenderObject(this.onChange);
@ -29,11 +29,11 @@ class _MeasureSizeRenderObject extends RenderProxyBox {
void performLayout() { void performLayout() {
super.performLayout(); super.performLayout();
Size newSize = child.size; var newSize = child?.size;
if (oldSize == newSize) return; if (newSize == null || oldSize == newSize) return;
oldSize = newSize; oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
onChange(newSize); onChange(newSize);
}); });
} }
@ -41,9 +41,9 @@ class _MeasureSizeRenderObject extends RenderProxyBox {
class SliverMeasureExtent extends SingleChildRenderObjectWidget { class SliverMeasureExtent extends SingleChildRenderObjectWidget {
const SliverMeasureExtent({ const SliverMeasureExtent({
Key key, Key? key,
@required this.onChange, required this.onChange,
@required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
@override @override
@ -61,16 +61,16 @@ class _SliverMeasureExtentRenderObject extends RenderProxySliver {
void performLayout() { void performLayout() {
super.performLayout(); super.performLayout();
double newExent = child.geometry.scrollExtent; var newExent = child?.geometry?.scrollExtent;
if (_oldExtent == newExent) { if (newExent == null || _oldExtent == newExent) {
return; return;
} }
_oldExtent = newExent; _oldExtent = newExent;
WidgetsBinding.instance.addPostFrameCallback((_) => onChange(newExent)); WidgetsBinding.instance!.addPostFrameCallback((_) => onChange(newExent));
} }
final void Function(double) onChange; final void Function(double) onChange;
double _oldExtent; double? _oldExtent;
} }

View file

@ -5,11 +5,11 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid { class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid {
MeasurableSliverStaggeredGrid.extentBuilder({ MeasurableSliverStaggeredGrid.extentBuilder({
Key key, Key? key,
@required double maxCrossAxisExtent, required double maxCrossAxisExtent,
@required IndexedStaggeredTileBuilder staggeredTileBuilder, required IndexedStaggeredTileBuilder staggeredTileBuilder,
@required IndexedWidgetBuilder itemBuilder, required IndexedWidgetBuilder itemBuilder,
@required int itemCount, required int itemCount,
double mainAxisSpacing = 0, double mainAxisSpacing = 0,
double crossAxisSpacing = 0, double crossAxisSpacing = 0,
}) : super( }) : super(
@ -32,19 +32,19 @@ class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid {
final element = context as SliverVariableSizeBoxAdaptorElement; final element = context as SliverVariableSizeBoxAdaptorElement;
_renderObject = RenderMeasurableSliverStaggeredGrid( _renderObject = RenderMeasurableSliverStaggeredGrid(
childManager: element, gridDelegate: gridDelegate); childManager: element, gridDelegate: gridDelegate);
return _renderObject; return _renderObject!;
} }
RenderMeasurableSliverStaggeredGrid get renderObject => _renderObject; RenderMeasurableSliverStaggeredGrid? get renderObject => _renderObject;
RenderMeasurableSliverStaggeredGrid _renderObject; RenderMeasurableSliverStaggeredGrid? _renderObject;
} }
class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid
with WidgetsBindingObserver { with WidgetsBindingObserver {
RenderMeasurableSliverStaggeredGrid({ RenderMeasurableSliverStaggeredGrid({
@required RenderSliverVariableSizeBoxChildManager childManager, required RenderSliverVariableSizeBoxChildManager childManager,
@required SliverStaggeredGridDelegate gridDelegate, required SliverStaggeredGridDelegate gridDelegate,
}) : super(childManager: childManager, gridDelegate: gridDelegate); }) : super(childManager: childManager, gridDelegate: gridDelegate);
/// Calculate the height of this staggered grid view /// Calculate the height of this staggered grid view
@ -69,13 +69,13 @@ class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid
} }
final bool hasTrailingScrollOffset = geometry.hasTrailingScrollOffset; final bool hasTrailingScrollOffset = geometry.hasTrailingScrollOffset;
RenderBox child; RenderBox? child;
if (!hasTrailingScrollOffset) { if (!hasTrailingScrollOffset) {
// Layout the child to compute its tailingScrollOffset. // Layout the child to compute its tailingScrollOffset.
final constraints = final constraints =
BoxConstraints.tightFor(width: geometry.crossAxisExtent); BoxConstraints.tightFor(width: geometry.crossAxisExtent);
child = addAndLayoutChild(index, constraints, parentUsesSize: true); child = addAndLayoutChild(index, constraints, parentUsesSize: true);
geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child)); geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child!));
} }
if (child != null) { if (child != null) {

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/language_util.dart' as language_util;
import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref.dart';
@ -44,10 +45,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
@override @override
build(BuildContext context) { build(BuildContext context) {
return MaterialApp( return MaterialApp(
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle, onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
theme: _getLightTheme(), theme: _getLightTheme(),
darkTheme: _getDarkTheme(), darkTheme: _getDarkTheme(),
themeMode: Pref.inst().isDarkTheme() ? ThemeMode.dark : ThemeMode.light, themeMode:
Pref.inst().isDarkThemeOr(false) ? ThemeMode.dark : ThemeMode.light,
initialRoute: Splash.routeName, initialRoute: Splash.routeName,
onGenerateRoute: _onGenerateRoute, onGenerateRoute: _onGenerateRoute,
navigatorObservers: <NavigatorObserver>[MyApp.routeObserver], navigatorObservers: <NavigatorObserver>[MyApp.routeObserver],
@ -87,9 +89,9 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
Splash.routeName: (context) => Splash(), Splash.routeName: (context) => Splash(),
}; };
Route<dynamic> _onGenerateRoute(RouteSettings settings) { Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
_log.info("[_onGenerateRoute] Route: ${settings.name}"); _log.info("[_onGenerateRoute] Route: ${settings.name}");
Route<dynamic> route; Route<dynamic>? route;
route ??= _handleBasicRoute(settings); route ??= _handleBasicRoute(settings);
route ??= _handleViewerRoute(settings); route ??= _handleViewerRoute(settings);
route ??= _handleConnectRoute(settings); route ??= _handleConnectRoute(settings);
@ -112,7 +114,7 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
setState(() {}); setState(() {});
} }
Route<dynamic> _handleBasicRoute(RouteSettings settings) { Route<dynamic>? _handleBasicRoute(RouteSettings settings) {
for (final e in _getRouter().entries) { for (final e in _getRouter().entries) {
if (e.key == settings.name) { if (e.key == settings.name) {
return MaterialPageRoute( return MaterialPageRoute(
@ -123,13 +125,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleViewerRoute(RouteSettings settings) { Route<dynamic>? _handleViewerRoute(RouteSettings settings) {
try { try {
if (settings.name == Viewer.routeName && settings.arguments != null) { if (settings.name == Viewer.routeName && settings.arguments != null) {
final ViewerArguments args = settings.arguments; final args = settings.arguments as ViewerArguments;
return MaterialPageRoute( return Viewer.buildRoute(args);
builder: (context) => Viewer.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleViewerRoute] Failed while handling route", e); _log.severe("[_handleViewerRoute] Failed while handling route", e);
@ -137,13 +137,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleConnectRoute(RouteSettings settings) { Route<dynamic>? _handleConnectRoute(RouteSettings settings) {
try { try {
if (settings.name == Connect.routeName && settings.arguments != null) { if (settings.name == Connect.routeName && settings.arguments != null) {
final ConnectArguments args = settings.arguments; final args = settings.arguments as ConnectArguments;
return MaterialPageRoute( return Connect.buildRoute(args);
builder: (context) => Connect.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleConnectRoute] Failed while handling route", e); _log.severe("[_handleConnectRoute] Failed while handling route", e);
@ -151,13 +149,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleHomeRoute(RouteSettings settings) { Route<dynamic>? _handleHomeRoute(RouteSettings settings) {
try { try {
if (settings.name == Home.routeName && settings.arguments != null) { if (settings.name == Home.routeName && settings.arguments != null) {
final HomeArguments args = settings.arguments; final args = settings.arguments as HomeArguments;
return MaterialPageRoute( return Home.buildRoute(args);
builder: (context) => Home.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleHomeRoute] Failed while handling route", e); _log.severe("[_handleHomeRoute] Failed while handling route", e);
@ -165,13 +161,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleRootPickerRoute(RouteSettings settings) { Route<dynamic>? _handleRootPickerRoute(RouteSettings settings) {
try { try {
if (settings.name == RootPicker.routeName && settings.arguments != null) { if (settings.name == RootPicker.routeName && settings.arguments != null) {
final RootPickerArguments args = settings.arguments; final args = settings.arguments as RootPickerArguments;
return MaterialPageRoute( return RootPicker.buildRoute(args);
builder: (context) => RootPicker.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleRootPickerRoute] Failed while handling route", e); _log.severe("[_handleRootPickerRoute] Failed while handling route", e);
@ -179,14 +173,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleAlbumViewerRoute(RouteSettings settings) { Route<dynamic>? _handleAlbumViewerRoute(RouteSettings settings) {
try { try {
if (settings.name == AlbumViewer.routeName && if (settings.name == AlbumViewer.routeName &&
settings.arguments != null) { settings.arguments != null) {
final AlbumViewerArguments args = settings.arguments; final args = settings.arguments as AlbumViewerArguments;
return MaterialPageRoute( return AlbumViewer.buildRoute(args);
builder: (context) => AlbumViewer.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleAlbumViewerRoute] Failed while handling route", e); _log.severe("[_handleAlbumViewerRoute] Failed while handling route", e);
@ -194,13 +186,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleSettingsRoute(RouteSettings settings) { Route<dynamic>? _handleSettingsRoute(RouteSettings settings) {
try { try {
if (settings.name == Settings.routeName && settings.arguments != null) { if (settings.name == Settings.routeName && settings.arguments != null) {
final SettingsArguments args = settings.arguments; final args = settings.arguments as SettingsArguments;
return MaterialPageRoute( return Settings.buildRoute(args);
builder: (context) => Settings.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleSettingsRoute] Failed while handling route", e); _log.severe("[_handleSettingsRoute] Failed while handling route", e);
@ -208,14 +198,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleArchiveViewerRoute(RouteSettings settings) { Route<dynamic>? _handleArchiveViewerRoute(RouteSettings settings) {
try { try {
if (settings.name == ArchiveViewer.routeName && if (settings.name == ArchiveViewer.routeName &&
settings.arguments != null) { settings.arguments != null) {
final ArchiveViewerArguments args = settings.arguments; final args = settings.arguments as ArchiveViewerArguments;
return MaterialPageRoute( return ArchiveViewer.buildRoute(args);
builder: (context) => ArchiveViewer.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleArchiveViewerRoute] Failed while handling route", e); _log.severe("[_handleArchiveViewerRoute] Failed while handling route", e);
@ -223,14 +211,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleDynamicAlbumViewerRoute(RouteSettings settings) { Route<dynamic>? _handleDynamicAlbumViewerRoute(RouteSettings settings) {
try { try {
if (settings.name == DynamicAlbumViewer.routeName && if (settings.name == DynamicAlbumViewer.routeName &&
settings.arguments != null) { settings.arguments != null) {
final DynamicAlbumViewerArguments args = settings.arguments; final args = settings.arguments as DynamicAlbumViewerArguments;
return MaterialPageRoute( return DynamicAlbumViewer.buildRoute(args);
builder: (context) => DynamicAlbumViewer.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe( _log.severe(
@ -239,14 +225,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleAlbumDirPickerRoute(RouteSettings settings) { Route<dynamic>? _handleAlbumDirPickerRoute(RouteSettings settings) {
try { try {
if (settings.name == AlbumDirPicker.routeName && if (settings.name == AlbumDirPicker.routeName &&
settings.arguments != null) { settings.arguments != null) {
final AlbumDirPickerArguments args = settings.arguments; final args = settings.arguments as AlbumDirPickerArguments;
return MaterialPageRoute( return AlbumDirPicker.buildRoute(args);
builder: (context) => AlbumDirPicker.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe( _log.severe(
@ -255,14 +239,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
return null; return null;
} }
Route<dynamic> _handleAlbumImporterRoute(RouteSettings settings) { Route<dynamic>? _handleAlbumImporterRoute(RouteSettings settings) {
try { try {
if (settings.name == AlbumImporter.routeName && if (settings.name == AlbumImporter.routeName &&
settings.arguments != null) { settings.arguments != null) {
final AlbumImporterArguments args = settings.arguments; final args = settings.arguments as AlbumImporterArguments;
return MaterialPageRoute( return AlbumImporter.buildRoute(args);
builder: (context) => AlbumImporter.fromArgs(args),
);
} }
} catch (e) { } catch (e) {
_log.severe("[_handleAlbumImporterRoute] Failed while handling route", e); _log.severe("[_handleAlbumImporterRoute] Failed while handling route", e);
@ -272,8 +254,8 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
AppEventListener<ThemeChangedEvent> _themeChangedListener; late AppEventListener<ThemeChangedEvent> _themeChangedListener;
AppEventListener<LanguageChangedEvent> _langChangedListener; late AppEventListener<LanguageChangedEvent> _langChangedListener;
static final _log = Logger("widget.my_app.MyAppState"); static final _log = Logger("widget.my_app.MyAppState");
} }

View file

@ -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/cover_provider.dart';
import 'package:nc_photos/entity/album/provider.dart'; import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_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/use_case/create_album.dart';
import 'package:nc_photos/widget/album_dir_picker.dart'; import 'package:nc_photos/widget/album_dir_picker.dart';
@ -16,8 +17,8 @@ import 'package:nc_photos/widget/album_dir_picker.dart';
/// cancelled /// cancelled
class NewAlbumDialog extends StatefulWidget { class NewAlbumDialog extends StatefulWidget {
NewAlbumDialog({ NewAlbumDialog({
Key key, Key? key,
@required this.account, required this.account,
this.isAllowDynamic = true, this.isAllowDynamic = true,
}) : super(key: key); }) : super(key: key);
@ -39,7 +40,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
return Visibility( return Visibility(
visible: _isVisible, visible: _isVisible,
child: AlertDialog( child: AlertDialog(
title: Text(AppLocalizations.of(context).createAlbumTooltip), title: Text(AppLocalizations.of(context)!.createAlbumTooltip),
content: Form( content: Form(
key: _formKey, key: _formKey,
child: Container( child: Container(
@ -50,17 +51,17 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
children: [ children: [
TextFormField( TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context).nameInputHint, hintText: AppLocalizations.of(context)!.nameInputHint,
), ),
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value!.isEmpty) {
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.albumNameInputInvalidEmpty; .albumNameInputInvalidEmpty;
} }
return null; return null;
}, },
onSaved: (value) { onSaved: (value) {
_formValue.name = value; _formValue.name = value!;
}, },
), ),
if (widget.isAllowDynamic) ...[ if (widget.isAllowDynamic) ...[
@ -75,7 +76,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
.toList(), .toList(),
onChanged: (newValue) { onChanged: (newValue) {
setState(() { setState(() {
_provider = newValue; _provider = newValue!;
}); });
}, },
onSaved: (value) { onSaved: (value) {
@ -104,8 +105,8 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
} }
void _onOkPressed(BuildContext context) { void _onOkPressed(BuildContext context) {
if (_formKey.currentState.validate()) { if (_formKey.currentState?.validate() == true) {
_formKey.currentState.save(); _formKey.currentState!.save();
if (_formValue.provider == _Provider.static || if (_formValue.provider == _Provider.static ||
_formValue.provider == null) { _formValue.provider == null) {
_onConfirmStaticAlbum(); _onConfirmStaticAlbum();
@ -136,7 +137,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
_isVisible = false; _isVisible = false;
}); });
Navigator.of(context) Navigator.of(context)
.pushNamed(AlbumDirPicker.routeName, .pushNamed<List<File>>(AlbumDirPicker.routeName,
arguments: AlbumDirPickerArguments(widget.account)) arguments: AlbumDirPickerArguments(widget.account))
.then((value) { .then((value) {
if (value == null) { if (value == null) {
@ -174,8 +175,8 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
} }
class _FormValue { class _FormValue {
String name; late String name;
_Provider provider; _Provider? provider;
} }
enum _Provider { enum _Provider {
@ -187,10 +188,10 @@ extension on _Provider {
String toValueString(BuildContext context) { String toValueString(BuildContext context) {
switch (this) { switch (this) {
case _Provider.static: case _Provider.static:
return AppLocalizations.of(context).createAlbumDialogBasicLabel; return AppLocalizations.of(context)!.createAlbumDialogBasicLabel;
case _Provider.dir: case _Provider.dir:
return AppLocalizations.of(context).createAlbumDialogFolderBasedLabel; return AppLocalizations.of(context)!.createAlbumDialogFolderBasedLabel;
default: default:
throw StateError("Unknown value: $this"); throw StateError("Unknown value: $this");
@ -200,10 +201,10 @@ extension on _Provider {
String toDescription(BuildContext context) { String toDescription(BuildContext context) {
switch (this) { switch (this) {
case _Provider.static: case _Provider.static:
return AppLocalizations.of(context).createAlbumDialogBasicDescription; return AppLocalizations.of(context)!.createAlbumDialogBasicDescription;
case _Provider.dir: case _Provider.dir:
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.createAlbumDialogFolderBasedDescription; .createAlbumDialogFolderBasedDescription;
default: default:

View file

@ -5,7 +5,7 @@ mixin PageVisibilityMixin<T extends StatefulWidget> on State<T>, RouteAware {
@override @override
didChangeDependencies() { didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
MyApp.routeObserver.subscribe(this, ModalRoute.of(context)); MyApp.routeObserver.subscribe(this, ModalRoute.of(context)!);
} }
@override @override

View file

@ -6,8 +6,8 @@ import 'package:nc_photos/num_extension.dart';
class PhotoDateTimeEditDialog extends StatefulWidget { class PhotoDateTimeEditDialog extends StatefulWidget {
PhotoDateTimeEditDialog({ PhotoDateTimeEditDialog({
Key key, Key? key,
@required this.initialDateTime, required this.initialDateTime,
}) : super(key: key); }) : super(key: key);
@override @override
@ -20,7 +20,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
@override @override
build(BuildContext context) { build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).updateDateTimeDialogTitle), title: Text(AppLocalizations.of(context)!.updateDateTimeDialogTitle),
content: Form( content: Form(
key: _formKey, key: _formKey,
child: Container( child: Container(
@ -29,7 +29,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
AppLocalizations.of(context).dateSubtitle, AppLocalizations.of(context)!.dateSubtitle,
style: Theme.of(context).textTheme.subtitle2, style: Theme.of(context).textTheme.subtitle2,
), ),
Row( Row(
@ -38,20 +38,20 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
child: TextFormField( child: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText:
AppLocalizations.of(context).dateYearInputHint, AppLocalizations.of(context)!.dateYearInputHint,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) { validator: (value) {
try { try {
int.parse(value); int.parse(value!);
return null; return null;
} catch (_) { } catch (_) {
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.dateTimeInputInvalid; .dateTimeInputInvalid;
} }
}, },
onSaved: (value) { onSaved: (value) {
_formValue.year = int.parse(value); _formValue.year = int.parse(value!);
}, },
initialValue: "${widget.initialDateTime.year}", initialValue: "${widget.initialDateTime.year}",
), ),
@ -62,18 +62,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
child: TextFormField( child: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText:
AppLocalizations.of(context).dateMonthInputHint, AppLocalizations.of(context)!.dateMonthInputHint,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) { validator: (value) {
if (int.tryParse(value)?.inRange(1, 12) == true) { if (int.tryParse(value!)?.inRange(1, 12) == true) {
return null; return null;
} }
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.dateTimeInputInvalid; .dateTimeInputInvalid;
}, },
onSaved: (value) { onSaved: (value) {
_formValue.month = int.parse(value); _formValue.month = int.parse(value!);
}, },
initialValue: widget.initialDateTime.month initialValue: widget.initialDateTime.month
.toString() .toString()
@ -85,18 +85,19 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
Flexible( Flexible(
child: TextFormField( child: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context).dateDayInputHint, hintText:
AppLocalizations.of(context)!.dateDayInputHint,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) { validator: (value) {
if (int.tryParse(value)?.inRange(1, 31) == true) { if (int.tryParse(value!)?.inRange(1, 31) == true) {
return null; return null;
} }
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.dateTimeInputInvalid; .dateTimeInputInvalid;
}, },
onSaved: (value) { onSaved: (value) {
_formValue.day = int.parse(value); _formValue.day = int.parse(value!);
}, },
initialValue: initialValue:
widget.initialDateTime.day.toString().padLeft(2, "0"), widget.initialDateTime.day.toString().padLeft(2, "0"),
@ -107,7 +108,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
AppLocalizations.of(context).timeSubtitle, AppLocalizations.of(context)!.timeSubtitle,
style: Theme.of(context).textTheme.subtitle2, style: Theme.of(context).textTheme.subtitle2,
), ),
Row( Row(
@ -116,18 +117,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
child: TextFormField( child: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText:
AppLocalizations.of(context).timeHourInputHint, AppLocalizations.of(context)!.timeHourInputHint,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) { validator: (value) {
if (int.tryParse(value)?.inRange(0, 23) == true) { if (int.tryParse(value!)?.inRange(0, 23) == true) {
return null; return null;
} }
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.dateTimeInputInvalid; .dateTimeInputInvalid;
}, },
onSaved: (value) { onSaved: (value) {
_formValue.hour = int.parse(value); _formValue.hour = int.parse(value!);
}, },
initialValue: widget.initialDateTime.hour initialValue: widget.initialDateTime.hour
.toString() .toString()
@ -140,18 +141,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
child: TextFormField( child: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText:
AppLocalizations.of(context).timeMinuteInputHint, AppLocalizations.of(context)!.timeMinuteInputHint,
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
validator: (value) { validator: (value) {
if (int.tryParse(value)?.inRange(0, 59) == true) { if (int.tryParse(value!)?.inRange(0, 59) == true) {
return null; return null;
} }
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.dateTimeInputInvalid; .dateTimeInputInvalid;
}, },
onSaved: (value) { onSaved: (value) {
_formValue.minute = int.parse(value); _formValue.minute = int.parse(value!);
}, },
initialValue: widget.initialDateTime.minute initialValue: widget.initialDateTime.minute
.toString() .toString()
@ -180,8 +181,8 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
} }
void _onSavePressed(BuildContext context) { void _onSavePressed(BuildContext context) {
if (_formKey.currentState.validate()) { if (_formKey.currentState?.validate() == true) {
_formKey.currentState.save(); _formKey.currentState!.save();
final d = DateTime(_formValue.year, _formValue.month, _formValue.day, final d = DateTime(_formValue.year, _formValue.month, _formValue.day,
_formValue.hour, _formValue.minute); _formValue.hour, _formValue.minute);
_log.info("[_onSavePressed] Set date time: $d"); _log.info("[_onSavePressed] Set date time: $d");
@ -197,9 +198,9 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
} }
class _FormValue { class _FormValue {
int year; late int year;
int month; late int month;
int day; late int day;
int hour; late int hour;
int minute; late int minute;
} }

View file

@ -7,9 +7,9 @@ import 'package:nc_photos/theme.dart';
class PhotoListImage extends StatelessWidget { class PhotoListImage extends StatelessWidget {
const PhotoListImage({ const PhotoListImage({
Key key, Key? key,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
this.isGif = false, this.isGif = false,
}) : super(key: key); }) : super(key: key);
@ -72,9 +72,9 @@ class PhotoListImage extends StatelessWidget {
class PhotoListVideo extends StatelessWidget { class PhotoListVideo extends StatelessWidget {
const PhotoListVideo({ const PhotoListVideo({
Key key, Key? key,
@required this.account, required this.account,
@required this.previewUrl, required this.previewUrl,
}) : super(key: key); }) : super(key: key);
@override @override

View file

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
class PopupMenuZoom extends PopupMenuEntry<void> { class PopupMenuZoom extends PopupMenuEntry<void> {
PopupMenuZoom({ PopupMenuZoom({
Key key, Key? key,
@required this.initialValue, required this.initialValue,
@required this.minValue, required this.minValue,
@required this.maxValue, required this.maxValue,
this.onChanged, this.onChanged,
}) : super(key: key); }) : super(key: key);
@ -22,7 +22,7 @@ class PopupMenuZoom extends PopupMenuEntry<void> {
final int initialValue; final int initialValue;
final double minValue; final double minValue;
final double maxValue; final double maxValue;
final void Function(double) onChanged; final void Function(double)? onChanged;
} }
class _PopupMenuZoomState extends State<PopupMenuZoom> { class _PopupMenuZoomState extends State<PopupMenuZoom> {

View file

@ -3,8 +3,8 @@ import 'package:flutter/widgets.dart';
class ProcessingDialog extends StatelessWidget { class ProcessingDialog extends StatelessWidget {
ProcessingDialog({ ProcessingDialog({
Key key, Key? key,
@required this.text, required this.text,
}); });
@override @override

View file

@ -25,12 +25,17 @@ class RootPickerArguments {
class RootPicker extends StatefulWidget { class RootPicker extends StatefulWidget {
static const routeName = "/root-picker"; static const routeName = "/root-picker";
static Route buildRoute(RootPickerArguments args) =>
MaterialPageRoute<Account>(
builder: (context) => RootPicker.fromArgs(args),
);
RootPicker({ RootPicker({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
RootPicker.fromArgs(RootPickerArguments args, {Key key}) RootPicker.fromArgs(RootPickerArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -100,7 +105,7 @@ class _RootPickerState extends State<RootPicker>
child: Column( child: Column(
children: [ children: [
Text( Text(
AppLocalizations.of(context).rootPickerHeaderText, AppLocalizations.of(context)!.rootPickerHeaderText,
style: Theme.of(context).textTheme.headline5, style: Theme.of(context).textTheme.headline5,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -108,7 +113,7 @@ class _RootPickerState extends State<RootPicker>
Align( Align(
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
child: Text( child: Text(
AppLocalizations.of(context).rootPickerSubHeaderText, AppLocalizations.of(context)!.rootPickerSubHeaderText,
), ),
), ),
], ],
@ -127,11 +132,11 @@ class _RootPickerState extends State<RootPicker>
children: [ children: [
TextButton( TextButton(
onPressed: () => _onSkipPressed(context), onPressed: () => _onSkipPressed(context),
child: Text(AppLocalizations.of(context).skipButtonLabel), child: Text(AppLocalizations.of(context)!.skipButtonLabel),
), ),
ElevatedButton( ElevatedButton(
onPressed: () => _onConfirmPressed(context), 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( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.rootPickerSkipConfirmationDialogContent), .rootPickerSkipConfirmationDialogContent),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
@ -174,7 +179,7 @@ class _RootPickerState extends State<RootPicker>
if (roots.isEmpty) { if (roots.isEmpty) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: content:
Text(AppLocalizations.of(context).rootPickerListEmptyNotification), Text(AppLocalizations.of(context)!.rootPickerListEmptyNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
return; return;
@ -189,12 +194,12 @@ class _RootPickerState extends State<RootPicker>
return; return;
} }
_isInitDialogShown = true; _isInitDialogShown = true;
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance!.addPostFrameCallback((_) {
showDialog( showDialog(
barrierDismissible: false, barrierDismissible: false,
context: context, context: context,
builder: (context) => ProcessingDialog( builder: (context) => ProcessingDialog(
text: AppLocalizations.of(context).genericProcessingDialogContent), text: AppLocalizations.of(context)!.genericProcessingDialogContent),
); );
}); });
} }

View file

@ -5,10 +5,10 @@ import 'package:nc_photos/theme.dart';
// Overlay a check mark if an item is selected // Overlay a check mark if an item is selected
class Selectable extends StatelessWidget { class Selectable extends StatelessWidget {
Selectable({ Selectable({
Key key, Key? key,
@required this.child, required this.child,
this.isSelected = false, this.isSelected = false,
@required this.iconSize, required this.iconSize,
this.borderRadius, this.borderRadius,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
@ -55,8 +55,8 @@ class Selectable extends StatelessWidget {
final Widget child; final Widget child;
final bool isSelected; final bool isSelected;
final double iconSize; final double iconSize;
final BorderRadiusGeometry borderRadius; final BorderRadius? borderRadius;
final VoidCallback onTap; final VoidCallback? onTap;
final VoidCallback onLongPress; final VoidCallback? onLongPress;
} }

View file

@ -18,7 +18,7 @@ import 'package:nc_photos/widget/selectable.dart';
abstract class SelectableItem { abstract class SelectableItem {
Widget buildWidget(BuildContext context); Widget buildWidget(BuildContext context);
VoidCallback get onTap => null; VoidCallback? get onTap => null;
bool get isSelectable => false; bool get isSelectable => false;
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1); StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
} }
@ -33,7 +33,7 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
@protected @protected
Widget buildItemStreamListOuter( Widget buildItemStreamListOuter(
BuildContext context, { BuildContext context, {
@required Widget child, required Widget child,
}) { }) {
if (platform_k.isWeb) { if (platform_k.isWeb) {
// support shift+click group selection on web // support shift+click group selection on web
@ -51,8 +51,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
@protected @protected
Widget buildItemStreamList({ Widget buildItemStreamList({
@required double maxCrossAxisExtent, required double maxCrossAxisExtent,
ValueChanged<double> onMaxExtentChanged, ValueChanged<double?>? onMaxExtentChanged,
}) { }) {
return MeasurableItemList( return MeasurableItemList(
key: _listKey, key: _listKey,
@ -81,24 +81,25 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
@protected @protected
set itemStreamListItems(List<SelectableItem> newItems) { set itemStreamListItems(List<SelectableItem> newItems) {
final lastSelectedItem = final lastSelectedItem =
_lastSelectPosition != null ? _items[_lastSelectPosition] : null; _lastSelectPosition != null ? _items[_lastSelectPosition!] : null;
_items = newItems; _items = newItems;
_transformSelectedItems(); _transformSelectedItems();
// Keep _lastSelectPosition if no changes, drop otherwise // Keep _lastSelectPosition if no changes, drop otherwise
int newLastSelectPosition; int? newLastSelectPosition;
try { try {
if (lastSelectedItem != null && if (lastSelectedItem != null &&
lastSelectedItem == _items[_lastSelectPosition]) { lastSelectedItem == _items[_lastSelectPosition!]) {
newLastSelectPosition = _lastSelectPosition; newLastSelectPosition = _lastSelectPosition!;
} }
} catch (_) {} } catch (_) {}
_lastSelectPosition = newLastSelectPosition; _lastSelectPosition = newLastSelectPosition;
_log.info("[itemStreamListItems] updateListHeight: list item changed"); _log.info("[itemStreamListItems] updateListHeight: list item changed");
WidgetsBinding.instance.addPostFrameCallback((_) => WidgetsBinding.instance!.addPostFrameCallback((_) =>
(_listKey.currentState as MeasurableItemListState)?.updateListHeight()); (_listKey.currentState as MeasurableItemListState?)
?.updateListHeight());
} }
Widget _buildItem(BuildContext context, int index) { Widget _buildItem(BuildContext context, int index) {
@ -139,8 +140,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
if (_isRangeSelectionMode && _lastSelectPosition != null) { if (_isRangeSelectionMode && _lastSelectPosition != null) {
setState(() { setState(() {
_selectedItems.addAll(_items _selectedItems.addAll(_items
.sublist(math.min(_lastSelectPosition, index), .sublist(math.min(_lastSelectPosition!, index),
math.max(_lastSelectPosition, index) + 1) math.max(_lastSelectPosition!, index) + 1)
.where((e) => e.isSelectable)); .where((e) => e.isSelectable));
_lastSelectPosition = index; _lastSelectPosition = index;
}); });
@ -166,8 +167,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
if (!platform_k.isWeb && wasSelectionMode && _lastSelectPosition != null) { if (!platform_k.isWeb && wasSelectionMode && _lastSelectPosition != null) {
setState(() { setState(() {
_selectedItems.addAll(_items _selectedItems.addAll(_items
.sublist(math.min(_lastSelectPosition, index), .sublist(math.min(_lastSelectPosition!, index),
math.max(_lastSelectPosition, index) + 1) math.max(_lastSelectPosition!, index) + 1)
.where((e) => e.isSelectable)); .where((e) => e.isSelectable));
_lastSelectPosition = index; _lastSelectPosition = index;
}); });
@ -183,8 +184,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
if (!SessionStorage().hasShowRangeSelectNotification) { if (!SessionStorage().hasShowRangeSelectNotification) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(platform_k.isWeb content: Text(platform_k.isWeb
? AppLocalizations.of(context).webSelectRangeNotification ? AppLocalizations.of(context)!.webSelectRangeNotification
: AppLocalizations.of(context).mobileSelectRangeNotification), : AppLocalizations.of(context)!.mobileSelectRangeNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
SessionStorage().hasShowRangeSelectNotification = true; SessionStorage().hasShowRangeSelectNotification = true;
@ -205,14 +206,14 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
return null; return null;
} }
}) })
.where((element) => element != null) .whereType<SelectableItem>()
.toList(); .toList();
_selectedItems _selectedItems
..clear() ..clear()
..addAll(newSelectedItems); ..addAll(newSelectedItems);
} }
int _lastSelectPosition; int? _lastSelectPosition;
bool _isRangeSelectionMode = false; bool _isRangeSelectionMode = false;
var _items = <SelectableItem>[]; var _items = <SelectableItem>[];

View file

@ -23,12 +23,16 @@ class SettingsArguments {
class Settings extends StatefulWidget { class Settings extends StatefulWidget {
static const routeName = "/settings"; static const routeName = "/settings";
static Route buildRoute(SettingsArguments args) => MaterialPageRoute(
builder: (context) => Settings.fromArgs(args),
);
Settings({ Settings({
Key key, Key? key,
@required this.account, required this.account,
}) : super(key: key); }) : super(key: key);
Settings.fromArgs(SettingsArguments args, {Key key}) Settings.fromArgs(SettingsArguments args, {Key? key})
: this( : this(
account: args.account, account: args.account,
); );
@ -43,7 +47,7 @@ class _SettingsState extends State<Settings> {
@override @override
initState() { initState() {
super.initState(); super.initState();
_isEnableExif = Pref.inst().isEnableExif(); _isEnableExif = Pref.inst().isEnableExifOr();
} }
@override @override
@ -58,40 +62,41 @@ class _SettingsState extends State<Settings> {
} }
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final translator = AppLocalizations.of(context).translator; final translator = AppLocalizations.of(context)!.translator;
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
pinned: true, pinned: true,
title: Text(AppLocalizations.of(context).settingsWidgetTitle), title: Text(AppLocalizations.of(context)!.settingsWidgetTitle),
), ),
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
ListTile( ListTile(
title: Text(AppLocalizations.of(context).settingsLanguageTitle), title:
Text(AppLocalizations.of(context)!.settingsLanguageTitle),
subtitle: Text(language_util.getSelectedLanguageName(context)), subtitle: Text(language_util.getSelectedLanguageName(context)),
onTap: () => _onLanguageTap(context), onTap: () => _onLanguageTap(context),
), ),
SwitchListTile( SwitchListTile(
title: title: Text(
Text(AppLocalizations.of(context).settingsExifSupportTitle), AppLocalizations.of(context)!.settingsExifSupportTitle),
subtitle: _isEnableExif subtitle: _isEnableExif
? Text(AppLocalizations.of(context) ? Text(AppLocalizations.of(context)!
.settingsExifSupportTrueSubtitle) .settingsExifSupportTrueSubtitle)
: null, : null,
value: _isEnableExif, value: _isEnableExif,
onChanged: (value) => _onExifSupportChanged(context, value), onChanged: (value) => _onExifSupportChanged(context, value),
), ),
_buildCaption(context, _buildCaption(context,
AppLocalizations.of(context).settingsAboutSectionTitle), AppLocalizations.of(context)!.settingsAboutSectionTitle),
ListTile( ListTile(
title: Text(AppLocalizations.of(context).settingsVersionTitle), title: Text(AppLocalizations.of(context)!.settingsVersionTitle),
subtitle: const Text(k.versionStr), subtitle: const Text(k.versionStr),
), ),
ListTile( ListTile(
title: title:
Text(AppLocalizations.of(context).settingsSourceCodeTitle), Text(AppLocalizations.of(context)!.settingsSourceCodeTitle),
subtitle: Text(_sourceRepo), subtitle: Text(_sourceRepo),
onTap: () async { onTap: () async {
await launch(_sourceRepo); await launch(_sourceRepo);
@ -99,7 +104,7 @@ class _SettingsState extends State<Settings> {
), ),
ListTile( ListTile(
title: title:
Text(AppLocalizations.of(context).settingsBugReportTitle), Text(AppLocalizations.of(context)!.settingsBugReportTitle),
onTap: () { onTap: () {
launch(_bugReportUrl); launch(_bugReportUrl);
}, },
@ -107,7 +112,7 @@ class _SettingsState extends State<Settings> {
if (translator.isNotEmpty) if (translator.isNotEmpty)
ListTile( ListTile(
title: Text( title: Text(
AppLocalizations.of(context).settingsTranslatorTitle), AppLocalizations.of(context)!.settingsTranslatorTitle),
subtitle: Text(translator), subtitle: Text(translator),
onTap: () { onTap: () {
launch(_translationUrl); launch(_translationUrl);
@ -172,8 +177,8 @@ class _SettingsState extends State<Settings> {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text( title: Text(
AppLocalizations.of(context).exifSupportConfirmationDialogTitle), AppLocalizations.of(context)!.exifSupportConfirmationDialogTitle),
content: Text(AppLocalizations.of(context).exifSupportDetails), content: Text(AppLocalizations.of(context)!.exifSupportDetails),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -185,7 +190,7 @@ class _SettingsState extends State<Settings> {
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); 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"); _log.severe("[_setExifSupport] Failed writing pref");
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).writePreferenceFailureNotification), AppLocalizations.of(context)!.writePreferenceFailureNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
setState(() { setState(() {
@ -231,7 +236,7 @@ class _SettingsState extends State<Settings> {
static const String _translationUrl = static const String _translationUrl =
"https://gitlab.com/nkming2/nc-photos/-/tree/master/lib/l10n"; "https://gitlab.com/nkming2/nc-photos/-/tree/master/lib/l10n";
bool _isEnableExif; late bool _isEnableExif;
static final _log = Logger("widget.settings._SettingsState"); static final _log = Logger("widget.settings._SettingsState");
} }

View file

@ -9,7 +9,7 @@ import 'package:nc_photos/widget/sign_in.dart';
import 'package:page_view_indicators/circle_page_indicator.dart'; import 'package:page_view_indicators/circle_page_indicator.dart';
bool isNeedSetup() => bool isNeedSetup() =>
Pref.inst().getSetupProgress() & _PageId.all != _PageId.all; Pref.inst().getSetupProgressOr() & _PageId.all != _PageId.all;
class Setup extends StatefulWidget { class Setup extends StatefulWidget {
static const routeName = "/setup"; static const routeName = "/setup";
@ -29,15 +29,15 @@ class _SetupState extends State<Setup> {
); );
} }
Widget _buildAppBar(BuildContext context) { PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar( return AppBar(
title: Text(AppLocalizations.of(context).setupWidgetTitle), title: Text(AppLocalizations.of(context)!.setupWidgetTitle),
elevation: 0, elevation: 0,
); );
} }
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final page = _pageController.hasClients ? _pageController.page.round() : 0; final page = _pageController.hasClients ? _pageController.page!.round() : 0;
final pages = <Widget>[ final pages = <Widget>[
if (_initialProgress & _PageId.exif == 0) _Exif(), if (_initialProgress & _PageId.exif == 0) _Exif(),
if (_initialProgress & _PageId.hiddenPrefDirNotice == 0) if (_initialProgress & _PageId.hiddenPrefDirNotice == 0)
@ -70,16 +70,21 @@ class _SetupState extends State<Setup> {
ElevatedButton( ElevatedButton(
onPressed: _onDonePressed, onPressed: _onDonePressed,
child: Text( child: Text(
AppLocalizations.of(context).doneButtonLabel), AppLocalizations.of(context)!.doneButtonLabel),
), ),
] ]
: [ : [
ElevatedButton( ElevatedButton(
onPressed: () => _onNextPressed( onPressed: () {
(pages[_pageController.page.round()] as _Page) if (_pageController.hasClients) {
.getPageId()), _onNextPressed(
(pages[_pageController.page!.round()]
as _Page)
.getPageId());
}
},
child: Text( child: Text(
AppLocalizations.of(context).nextButtonLabel), AppLocalizations.of(context)!.nextButtonLabel),
), ),
], ],
), ),
@ -107,12 +112,12 @@ class _SetupState extends State<Setup> {
} }
void _onNextPressed(int pageId) { void _onNextPressed(int pageId) {
Pref.inst().setSetupProgress(Pref.inst().getSetupProgress() | pageId); Pref.inst().setSetupProgress(Pref.inst().getSetupProgressOr() | pageId);
_pageController.nextPage( _pageController.nextPage(
duration: k.animationDurationNormal, curve: Curves.easeInOut); duration: k.animationDurationNormal, curve: Curves.easeInOut);
} }
final _initialProgress = Pref.inst().getSetupProgress(); final _initialProgress = Pref.inst().getSetupProgressOr();
final _pageController = PageController(); final _pageController = PageController();
var _currentPageNotifier = ValueNotifier<int>(0); var _currentPageNotifier = ValueNotifier<int>(0);
} }
@ -143,23 +148,23 @@ class _ExifState extends State<_Exif> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context).settingsExifSupportTitle), title: Text(AppLocalizations.of(context)!.settingsExifSupportTitle),
value: _isEnableExif, value: _isEnableExif,
onChanged: _onValueChanged, onChanged: _onValueChanged,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(AppLocalizations.of(context).exifSupportDetails), child: Text(AppLocalizations.of(context)!.exifSupportDetails),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text( child: Text(
AppLocalizations.of(context).setupSettingsModifyLaterHint, AppLocalizations.of(context)!.setupSettingsModifyLaterHint,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyText2 .bodyText2!
.copyWith(fontStyle: FontStyle.italic)), .copyWith(fontStyle: FontStyle.italic)),
), ),
const SizedBox(height: 8), 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 { class _HiddenPrefDirNotice extends StatefulWidget implements _Page {
@ -196,7 +201,7 @@ class _HiddenPrefDirNoticeState extends State<_HiddenPrefDirNotice> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text( child: Text(
AppLocalizations.of(context).setupHiddenPrefDirNoticeDetail), AppLocalizations.of(context)!.setupHiddenPrefDirNoticeDetail),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Align( Align(

View file

@ -16,7 +16,7 @@ import 'package:nc_photos/widget/root_picker.dart';
class SignIn extends StatefulWidget { class SignIn extends StatefulWidget {
static const routeName = "/sign-in"; static const routeName = "/sign-in";
SignIn({Key key}) : super(key: key); SignIn({Key? key}) : super(key: key);
@override @override
createState() => _SignInState(); createState() => _SignInState();
@ -49,7 +49,7 @@ class _SignInState extends State<SignIn> {
Padding( Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Text( child: Text(
AppLocalizations.of(context).signInHeaderText, AppLocalizations.of(context)!.signInHeaderText,
style: Theme.of(context).textTheme.headline5, style: Theme.of(context).textTheme.headline5,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -70,7 +70,7 @@ class _SignInState extends State<SignIn> {
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 32, vertical: 16), horizontal: 32, vertical: 16),
child: Text( child: Text(
AppLocalizations.of(context).signIn2faHintText, AppLocalizations.of(context)!.signIn2faHintText,
style: TextStyle(fontStyle: FontStyle.italic), style: TextStyle(fontStyle: FontStyle.italic),
), ),
), ),
@ -82,7 +82,7 @@ class _SignInState extends State<SignIn> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (!ModalRoute.of(context).isFirst) if (!ModalRoute.of(context)!.isFirst)
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@ -94,11 +94,11 @@ class _SignInState extends State<SignIn> {
Container(), Container(),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
if (_formKey.currentState.validate()) { if (_formKey.currentState?.validate() == true) {
_connect(); _connect();
} }
}, },
child: Text(AppLocalizations.of(context) child: Text(AppLocalizations.of(context)!
.connectButtonLabel), .connectButtonLabel),
), ),
], ],
@ -143,11 +143,11 @@ class _SignInState extends State<SignIn> {
.toList(), .toList(),
onChanged: (newValue) { onChanged: (newValue) {
setState(() { setState(() {
_scheme = newValue; _scheme = newValue!;
}); });
}, },
onSaved: (value) { onSaved: (value) {
_formValue.scheme = value.toValueString(); _formValue.scheme = value!.toValueString();
}, },
), ),
), ),
@ -159,18 +159,19 @@ class _SignInState extends State<SignIn> {
Expanded( Expanded(
child: TextFormField( child: TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context).serverAddressInputHint, hintText:
AppLocalizations.of(context)!.serverAddressInputHint,
), ),
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
validator: (value) { validator: (value) {
if (value.trim().trimRightAny("/").isEmpty) { if (value!.trim().trimRightAny("/").isEmpty) {
return AppLocalizations.of(context) return AppLocalizations.of(context)!
.serverAddressInputInvalidEmpty; .serverAddressInputInvalidEmpty;
} }
return null; return null;
}, },
onSaved: (value) { 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), const SizedBox(height: 8),
TextFormField( TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context).usernameInputHint, hintText: AppLocalizations.of(context)!.usernameInputHint,
), ),
validator: (value) { validator: (value) {
if (value.trim().isEmpty) { if (value!.trim().isEmpty) {
return AppLocalizations.of(context).usernameInputInvalidEmpty; return AppLocalizations.of(context)!.usernameInputInvalidEmpty;
} }
return null; return null;
}, },
onSaved: (value) { onSaved: (value) {
_formValue.username = value; _formValue.username = value!;
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
TextFormField( TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context).passwordInputHint, hintText: AppLocalizations.of(context)!.passwordInputHint,
), ),
obscureText: true, obscureText: true,
validator: (value) { validator: (value) {
if (value.trim().isEmpty) { if (value!.trim().isEmpty) {
return AppLocalizations.of(context).passwordInputInvalidEmpty; return AppLocalizations.of(context)!.passwordInputInvalidEmpty;
} }
return null; return null;
}, },
onSaved: (value) { onSaved: (value) {
_formValue.password = value; _formValue.password = value!;
}, },
), ),
], ],
@ -212,22 +213,23 @@ class _SignInState extends State<SignIn> {
} }
void _connect() { void _connect() {
_formKey.currentState.save(); _formKey.currentState!.save();
final account = Account(_formValue.scheme, _formValue.address, final account = Account(_formValue.scheme, _formValue.address,
_formValue.username, _formValue.password, [""]); _formValue.username, _formValue.password, [""]);
_log.info("[_connect] Try connecting with account: $account"); _log.info("[_connect] Try connecting with account: $account");
Navigator.pushNamed(context, Connect.routeName, Navigator.pushNamed<Account>(context, Connect.routeName,
arguments: ConnectArguments(account)) arguments: ConnectArguments(account))
.then((result) { .then<Account?>((result) {
return result != null return result != null
? Navigator.pushNamed(context, RootPicker.routeName, ? Navigator.pushNamed(context, RootPicker.routeName,
arguments: RootPickerArguments(result)) arguments: RootPickerArguments(result))
: null; : Future.value(null);
}).then((result) { }).then((result) {
if (result != null) { if (result != null) {
// we've got a good account // we've got a good account
// only signing in with app password would trigger distinct // 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() Pref.inst()
..setAccounts(accounts) ..setAccounts(accounts)
..setCurrentAccountIndex(accounts.indexOf(result)); ..setCurrentAccountIndex(accounts.indexOf(result));
@ -267,8 +269,8 @@ extension on _Scheme {
} }
class _FormValue { class _FormValue {
String scheme; late String scheme;
String address; late String address;
String username; late String username;
String password; late String password;
} }

View file

@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
class SimpleInputDialog extends StatefulWidget { class SimpleInputDialog extends StatefulWidget {
SimpleInputDialog({ SimpleInputDialog({
Key key, Key? key,
this.initialText, this.initialText,
this.hintText, this.hintText,
this.validator, this.validator,
@ -12,9 +12,9 @@ class SimpleInputDialog extends StatefulWidget {
@override @override
createState() => _SimpleInputDialogState(); createState() => _SimpleInputDialogState();
final String initialText; final String? initialText;
final String hintText; final String? hintText;
final FormFieldValidator<String> validator; final FormFieldValidator<String>? validator;
} }
class _SimpleInputDialogState extends State<SimpleInputDialog> { class _SimpleInputDialogState extends State<SimpleInputDialog> {
@ -29,7 +29,7 @@ class _SimpleInputDialogState extends State<SimpleInputDialog> {
: InputDecoration(hintText: widget.hintText), : InputDecoration(hintText: widget.hintText),
validator: widget.validator, validator: widget.validator,
onSaved: (value) { onSaved: (value) {
_formValue.text = value; _formValue.text = value!;
}, },
initialValue: widget.initialText, initialValue: widget.initialText,
), ),
@ -48,15 +48,15 @@ class _SimpleInputDialogState extends State<SimpleInputDialog> {
void _onSavePressed() { void _onSavePressed() {
if (_formKey.currentState?.validate() == true) { if (_formKey.currentState?.validate() == true) {
_formValue = _FormValue(); _formValue = _FormValue();
_formKey.currentState.save(); _formKey.currentState!.save();
Navigator.of(context).pop(_formValue.text); Navigator.of(context).pop(_formValue.text);
} }
} }
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
_FormValue _formValue; var _formValue = _FormValue();
} }
class _FormValue { class _FormValue {
String text; late String text;
} }

View file

@ -13,7 +13,7 @@ import 'package:nc_photos/widget/sign_in.dart';
class Splash extends StatefulWidget { class Splash extends StatefulWidget {
static const routeName = "/splash"; static const routeName = "/splash";
Splash({Key key}) : super(key: key); Splash({Key? key}) : super(key: key);
@override @override
createState() => _SplashState(); createState() => _SplashState();
@ -23,7 +23,7 @@ class _SplashState extends State<Splash> {
@override @override
initState() { initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
if (_shouldUpgrade()) { if (_shouldUpgrade()) {
_handleUpgrade(); _handleUpgrade();
} else { } else {
@ -56,7 +56,7 @@ class _SplashState extends State<Splash> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
AppLocalizations.of(context).appTitle, AppLocalizations.of(context)!.appTitle,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline4, style: Theme.of(context).textTheme.headline4,
) )
@ -81,12 +81,12 @@ class _SplashState extends State<Splash> {
} }
bool _shouldUpgrade() { bool _shouldUpgrade() {
final lastVersion = Pref.inst().getLastVersion(k.version); final lastVersion = Pref.inst().getLastVersionOr(k.version);
return lastVersion < k.version; return lastVersion < k.version;
} }
void _handleUpgrade() { void _handleUpgrade() {
final lastVersion = Pref.inst().getLastVersion(k.version); final lastVersion = Pref.inst().getLastVersionOr(k.version);
// ... // ...
final change = _gatherChangelog(lastVersion); final change = _gatherChangelog(lastVersion);
@ -94,7 +94,7 @@ class _SplashState extends State<Splash> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).changelogTitle), title: Text(AppLocalizations.of(context)!.changelogTitle),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Text(change), child: Text(change),
), ),

View file

@ -12,8 +12,8 @@ import 'package:wakelock/wakelock.dart';
class VideoViewer extends StatefulWidget { class VideoViewer extends StatefulWidget {
VideoViewer({ VideoViewer({
@required this.account, required this.account,
@required this.file, required this.file,
this.onLoaded, this.onLoaded,
this.onHeightChanged, this.onHeightChanged,
this.onPlay, this.onPlay,
@ -27,10 +27,10 @@ class VideoViewer extends StatefulWidget {
final Account account; final Account account;
final File file; final File file;
final VoidCallback onLoaded; final VoidCallback? onLoaded;
final void Function(double height) onHeightChanged; final ValueChanged<double>? onHeightChanged;
final VoidCallback onPlay; final VoidCallback? onPlay;
final VoidCallback onPause; final VoidCallback? onPause;
final bool isControlVisible; final bool isControlVisible;
final bool canPlay; final bool canPlay;
} }
@ -47,9 +47,9 @@ class _VideoViewerState extends State<VideoViewer> {
)..initialize().then((_) { )..initialize().then((_) {
widget.onLoaded?.call(); widget.onLoaded?.call();
setState(() {}); setState(() {});
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
if (_key.currentContext != null) { if (_key.currentContext != null) {
widget.onHeightChanged?.call(_key.currentContext.size.height); widget.onHeightChanged?.call(_key.currentContext!.size!.height);
} }
}); });
}).catchError((e, stacktrace) { }).catchError((e, stacktrace) {
@ -80,13 +80,13 @@ class _VideoViewerState extends State<VideoViewer> {
@override @override
dispose() { dispose() {
super.dispose(); super.dispose();
_controller?.dispose(); _controller.dispose();
Wakelock.disable(); Wakelock.disable();
} }
Widget _buildPlayer(BuildContext context) { Widget _buildPlayer(BuildContext context) {
if (_controller.value.isPlaying && !widget.canPlay) { if (_controller.value.isPlaying && !widget.canPlay) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
_pause(); _pause();
}); });
} }
@ -197,7 +197,7 @@ class _VideoViewerState extends State<VideoViewer> {
} }
final _key = GlobalKey(); final _key = GlobalKey();
VideoPlayerController _controller; late VideoPlayerController _controller;
var _isFinished = false; var _isFinished = false;
static final _log = Logger("widget.video_viewer._VideoViewerState"); static final _log = Logger("widget.video_viewer._VideoViewerState");

View file

@ -38,14 +38,18 @@ class ViewerArguments {
class Viewer extends StatefulWidget { class Viewer extends StatefulWidget {
static const routeName = "/viewer"; static const routeName = "/viewer";
static Route buildRoute(ViewerArguments args) => MaterialPageRoute(
builder: (context) => Viewer.fromArgs(args),
);
Viewer({ Viewer({
Key key, Key? key,
@required this.account, required this.account,
@required this.streamFiles, required this.streamFiles,
@required this.startIndex, required this.startIndex,
}) : super(key: key); }) : super(key: key);
Viewer.fromArgs(ViewerArguments args, {Key key}) Viewer.fromArgs(ViewerArguments args, {Key? key})
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
@ -124,7 +128,7 @@ class _ViewerState extends State<Viewer> {
children: [ children: [
Container(color: Colors.black), Container(color: Colors.black),
if (!_pageController.hasClients || if (!_pageController.hasClients ||
!_pageStates[_pageController.page.round()].hasLoaded) !_pageStates[_pageController.page!.round()]!.hasLoaded)
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: const CircularProgressIndicator(), child: const CircularProgressIndicator(),
@ -260,7 +264,7 @@ class _ViewerState extends State<Viewer> {
if (!_isDetailPaneActive && _canOpenDetailPane()) if (!_isDetailPaneActive && _canOpenDetailPane())
IconButton( IconButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
tooltip: AppLocalizations.of(context).detailsTooltip, tooltip: AppLocalizations.of(context)!.detailsTooltip,
onPressed: _onDetailsPressed, onPressed: _onDetailsPressed,
), ),
], ],
@ -307,7 +311,7 @@ class _ViewerState extends State<Viewer> {
Icons.share_outlined, Icons.share_outlined,
color: Colors.white.withOpacity(.87), color: Colors.white.withOpacity(.87),
), ),
tooltip: AppLocalizations.of(context).shareTooltip, tooltip: AppLocalizations.of(context)!.shareTooltip,
onPressed: () => _onSharePressed(context), onPressed: () => _onSharePressed(context),
), ),
), ),
@ -318,7 +322,7 @@ class _ViewerState extends State<Viewer> {
Icons.download_outlined, Icons.download_outlined,
color: Colors.white.withOpacity(.87), color: Colors.white.withOpacity(.87),
), ),
tooltip: AppLocalizations.of(context).downloadTooltip, tooltip: AppLocalizations.of(context)!.downloadTooltip,
onPressed: () => _onDownloadPressed(context), onPressed: () => _onDownloadPressed(context),
), ),
), ),
@ -329,7 +333,7 @@ class _ViewerState extends State<Viewer> {
Icons.delete_outlined, Icons.delete_outlined,
color: Colors.white.withOpacity(.87), color: Colors.white.withOpacity(.87),
), ),
tooltip: AppLocalizations.of(context).deleteTooltip, tooltip: AppLocalizations.of(context)!.deleteTooltip,
onPressed: () => _onDeletePressed(context), onPressed: () => _onDeletePressed(context),
), ),
), ),
@ -344,7 +348,7 @@ class _ViewerState extends State<Viewer> {
Widget _buildPage(BuildContext context, int index) { Widget _buildPage(BuildContext context, int index) {
if (_pageStates[index] == null) { if (_pageStates[index] == null) {
_onCreateNewPage(context, index); _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 // the page has been moved out of view and is now coming back
_log.fine("[_buildPage] Recreating page#$index"); _log.fine("[_buildPage] Recreating page#$index");
_onRecreatePageAfterMovedOut(context, index); _onRecreatePageAfterMovedOut(context, index);
@ -359,7 +363,7 @@ class _ViewerState extends State<Viewer> {
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (notif) => _onPageContentScrolled(notif, index), onNotification: (notif) => _onPageContentScrolled(notif, index),
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _pageStates[index].scrollController, controller: _pageStates[index]!.scrollController,
physics: physics:
_isDetailPaneActive ? null : const NeverScrollableScrollPhysics(), _isDetailPaneActive ? null : const NeverScrollableScrollPhysics(),
child: Stack( child: Stack(
@ -409,7 +413,7 @@ class _ViewerState extends State<Viewer> {
return _buildVideoView(context, index); return _buildVideoView(context, index);
} else { } else {
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}"); _log.shout("[_buildItemView] Unknown file format: ${file.contentType}");
_pageStates[index].itemHeight = 0; _pageStates[index]!.itemHeight = 0;
return Container(); return Container();
} }
} }
@ -452,7 +456,7 @@ class _ViewerState extends State<Viewer> {
return false; return false;
} }
if (notification is ScrollEndNotification) { if (notification is ScrollEndNotification) {
final scrollPos = _pageStates[index].scrollController.position; final scrollPos = _pageStates[index]!.scrollController.position;
if (scrollPos.pixels == 0) { if (scrollPos.pixels == 0) {
setState(() { setState(() {
_onDetailPaneClosed(); _onDetailPaneClosed();
@ -463,14 +467,15 @@ class _ViewerState extends State<Viewer> {
// upward, open the pane to its minimal size // upward, open the pane to its minimal size
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
setState(() { setState(() {
_openDetailPane(_pageController.page.toInt(), _openDetailPane(_pageController.page!.toInt(),
shouldAnimate: true); shouldAnimate: true);
}); });
}); });
} else if (scrollPos.userScrollDirection == ScrollDirection.forward) { } else if (scrollPos.userScrollDirection == ScrollDirection.forward) {
// downward, close the pane // downward, close the pane
Future.delayed(Duration.zero, () { 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) { void _onImageLoaded(int index) {
// currently pageview doesn't pre-load pages, we do it manually // currently pageview doesn't pre-load pages, we do it manually
// don't pre-load if user already navigated away // don't pre-load if user already navigated away
if (_pageController.page.round() == index && if (_pageController.page!.round() == index &&
!_pageStates[index].hasLoaded) { !_pageStates[index]!.hasLoaded) {
_log.info("[_onImageLoaded] Pre-loading nearby images"); _log.info("[_onImageLoaded] Pre-loading nearby images");
if (index > 0) { if (index > 0) {
final prevFile = widget.streamFiles[index - 1]; final prevFile = widget.streamFiles[index - 1];
@ -497,16 +502,16 @@ class _ViewerState extends State<Viewer> {
} }
} }
setState(() { setState(() {
_pageStates[index].hasLoaded = true; _pageStates[index]!.hasLoaded = true;
}); });
} }
} }
void _onVideoLoaded(int index) { void _onVideoLoaded(int index) {
if (_pageController.page.round() == index && if (_pageController.page!.round() == index &&
!_pageStates[index].hasLoaded) { !_pageStates[index]!.hasLoaded) {
setState(() { 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 /// Called when the page is being built after previously moved out of view
void _onRecreatePageAfterMovedOut(BuildContext context, int index) { void _onRecreatePageAfterMovedOut(BuildContext context, int index) {
if (_isShowDetailPane && !_isClosingDetailPane) { if (_isShowDetailPane && !_isClosingDetailPane) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
if (_pageStates[index].itemHeight != null) { if (_pageStates[index]!.itemHeight != null) {
setState(() { setState(() {
_openDetailPane(index); _openDetailPane(index);
}); });
} }
}); });
} else { } else {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
_pageStates[index].scrollController.jumpTo(0); _pageStates[index]!.scrollController.jumpTo(0);
}); });
} }
} }
@ -551,26 +556,26 @@ class _ViewerState extends State<Viewer> {
void _onDetailsPressed() { void _onDetailsPressed() {
if (!_isDetailPaneActive) { if (!_isDetailPaneActive) {
setState(() { setState(() {
_openDetailPane(_pageController.page.toInt(), shouldAnimate: true); _openDetailPane(_pageController.page!.toInt(), shouldAnimate: true);
}); });
} }
} }
void _onSharePressed(BuildContext context) { void _onSharePressed(BuildContext context) {
assert(platform_k.isAndroid); assert(platform_k.isAndroid);
final file = widget.streamFiles[_pageController.page.round()]; final file = widget.streamFiles[_pageController.page!.round()];
ShareHandler().shareFiles(context, widget.account, [file]); ShareHandler().shareFiles(context, widget.account, [file]);
} }
void _onDownloadPressed(BuildContext context) async { 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}"); _log.info("[_onDownloadPressed] Downloading file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar( var controller = SnackBarManager().showSnackBar(SnackBar(
content: content:
Text(AppLocalizations.of(context).downloadProcessingNotification), Text(AppLocalizations.of(context)!.downloadProcessingNotification),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
controller?.closed?.whenComplete(() { controller?.closed.whenComplete(() {
controller = null; controller = null;
}); });
dynamic result; dynamic result;
@ -581,7 +586,7 @@ class _ViewerState extends State<Viewer> {
_log.warning("[_onDownloadPressed] Permission not granted"); _log.warning("[_onDownloadPressed] Permission not granted");
controller?.close(); controller?.close();
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.downloadFailureNoPermissionNotification), .downloadFailureNoPermissionNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -591,8 +596,8 @@ class _ViewerState extends State<Viewer> {
"[_onDownloadPressed] Failed while downloadFile", e, stacktrace); "[_onDownloadPressed] Failed while downloadFile", e, stacktrace);
controller?.close(); controller?.close();
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: content: Text(
Text("${AppLocalizations.of(context).downloadFailureNotification}: " "${AppLocalizations.of(context)!.downloadFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -622,19 +627,19 @@ class _ViewerState extends State<Viewer> {
// fallback // fallback
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).downloadSuccessNotification), content: Text(AppLocalizations.of(context)!.downloadSuccessNotification),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
} }
void _onDeletePressed(BuildContext context) async { 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}"); _log.info("[_onDeletePressed] Removing file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar( var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).deleteProcessingNotification), content: Text(AppLocalizations.of(context)!.deleteProcessingNotification),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
controller?.closed?.whenComplete(() { controller?.closed.whenComplete(() {
controller = null; controller = null;
}); });
try { try {
@ -642,7 +647,7 @@ class _ViewerState extends State<Viewer> {
AlbumRepo(AlbumCachedDataSource()))(widget.account, file); AlbumRepo(AlbumCachedDataSource()))(widget.account, file);
controller?.close(); controller?.close();
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).deleteSuccessNotification), content: Text(AppLocalizations.of(context)!.deleteSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -655,7 +660,7 @@ class _ViewerState extends State<Viewer> {
controller?.close(); controller?.close();
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: content:
Text("${AppLocalizations.of(context).deleteFailureNotification}: " Text("${AppLocalizations.of(context)!.deleteFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -666,8 +671,9 @@ class _ViewerState extends State<Viewer> {
if (_pageStates[index]?.itemHeight == null) { if (_pageStates[index]?.itemHeight == null) {
return MediaQuery.of(context).size.height; return MediaQuery.of(context).size.height;
} else { } else {
return _pageStates[index].itemHeight + return _pageStates[index]!.itemHeight! +
(MediaQuery.of(context).size.height - _pageStates[index].itemHeight) / (MediaQuery.of(context).size.height -
_pageStates[index]!.itemHeight!) /
2 - 2 -
4; 4;
} }
@ -680,10 +686,10 @@ class _ViewerState extends State<Viewer> {
} }
void _updateItemHeight(int index, double height) { 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"); _log.fine("[_updateItemHeight] New height of item#$index: $height");
setState(() { setState(() {
_pageStates[index].itemHeight = height; _pageStates[index]!.itemHeight = height;
if (_isDetailPaneActive) { if (_isDetailPaneActive) {
_openDetailPane(index); _openDetailPane(index);
} }
@ -709,12 +715,12 @@ class _ViewerState extends State<Viewer> {
_isShowDetailPane = true; _isShowDetailPane = true;
_isDetailPaneActive = true; _isDetailPaneActive = true;
if (shouldAnimate) { if (shouldAnimate) {
_pageStates[index].scrollController.animateTo( _pageStates[index]!.scrollController.animateTo(
_calcDetailPaneOpenedScrollPosition(index), _calcDetailPaneOpenedScrollPosition(index),
duration: k.animationDurationNormal, duration: k.animationDurationNormal,
curve: Curves.easeOut); curve: Curves.easeOut);
} else { } else {
_pageStates[index] _pageStates[index]!
.scrollController .scrollController
.jumpTo(_calcDetailPaneOpenedScrollPosition(index)); .jumpTo(_calcDetailPaneOpenedScrollPosition(index));
} }
@ -723,7 +729,7 @@ class _ViewerState extends State<Viewer> {
void _closeDetailPane(int index, {bool shouldAnimate = false}) { void _closeDetailPane(int index, {bool shouldAnimate = false}) {
_isClosingDetailPane = true; _isClosingDetailPane = true;
if (shouldAnimate) { if (shouldAnimate) {
_pageStates[index].scrollController.animateTo(0, _pageStates[index]!.scrollController.animateTo(0,
duration: k.animationDurationNormal, curve: Curves.easeOut); duration: k.animationDurationNormal, curve: Curves.easeOut);
} }
} }
@ -739,7 +745,7 @@ class _ViewerState extends State<Viewer> {
.previousPage( .previousPage(
duration: k.animationDurationNormal, curve: Curves.easeInOut) duration: k.animationDurationNormal, curve: Curves.easeInOut)
.whenComplete( .whenComplete(
() => _updateNavigationState(_pageController.page.round())); () => _updateNavigationState(_pageController.page!.round()));
} }
/// Switch to the next image in the stream /// Switch to the next image in the stream
@ -747,7 +753,7 @@ class _ViewerState extends State<Viewer> {
_pageController _pageController
.nextPage(duration: k.animationDurationNormal, curve: Curves.easeInOut) .nextPage(duration: k.animationDurationNormal, curve: Curves.easeInOut)
.whenComplete( .whenComplete(
() => _updateNavigationState(_pageController.page.round())); () => _updateNavigationState(_pageController.page!.round()));
} }
/// Switch to the image on the "left", what that means depend on the current /// 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; var _isZoomed = false;
PageController _pageController; late PageController _pageController;
final _pageStates = <int, _PageState>{}; final _pageStates = <int, _PageState>{};
/// used to gain focus on web for keyboard support /// used to gain focus on web for keyboard support
@ -832,6 +838,6 @@ class _PageState {
_PageState(this.scrollController); _PageState(this.scrollController);
ScrollController scrollController; ScrollController scrollController;
double itemHeight; double? itemHeight;
bool hasLoaded = false; bool hasLoaded = false;
} }

View file

@ -32,9 +32,9 @@ import 'package:tuple/tuple.dart';
class ViewerDetailPane extends StatefulWidget { class ViewerDetailPane extends StatefulWidget {
const ViewerDetailPane({ const ViewerDetailPane({
Key key, Key? key,
@required this.account, required this.account,
@required this.file, required this.file,
}) : super(key: key); }) : super(key: key);
@override @override
@ -54,7 +54,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
_log.info("[initState] Metadata missing in File"); _log.info("[initState] Metadata missing in File");
} else { } else {
_log.info("[initState] Metadata exists in File"); _log.info("[initState] Metadata exists in File");
if (widget.file.metadata.exif != null) { if (widget.file.metadata!.exif != null) {
_initMetadata(); _initMetadata();
} }
} }
@ -73,29 +73,29 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
const space = " "; const space = " ";
if (widget.file.metadata?.imageWidth != null && if (widget.file.metadata?.imageWidth != null &&
widget.file.metadata?.imageHeight != null) { widget.file.metadata?.imageHeight != null) {
final pixelCount = final pixelCount = widget.file.metadata!.imageWidth! *
widget.file.metadata.imageWidth * widget.file.metadata.imageHeight; widget.file.metadata!.imageHeight!;
if (pixelCount >= 500000) { if (pixelCount >= 500000) {
final mpCount = pixelCount / 1000000.0; final mpCount = pixelCount / 1000000.0;
sizeSubStr += AppLocalizations.of(context) sizeSubStr += AppLocalizations.of(context)!
.megapixelCount(mpCount.toStringAsFixed(1)); .megapixelCount(mpCount.toStringAsFixed(1));
sizeSubStr += space; sizeSubStr += space;
} }
sizeSubStr += _byteSizeToString(widget.file.contentLength); sizeSubStr += _byteSizeToString(widget.file.contentLength ?? 0);
} }
String cameraSubStr = ""; String cameraSubStr = "";
if (_fNumber != null) { if (_fNumber != null) {
cameraSubStr += "f/${_fNumber.toStringAsFixed(1)}$space"; cameraSubStr += "f/${_fNumber!.toStringAsFixed(1)}$space";
} }
if (_exposureTime != null) { if (_exposureTime != null) {
cameraSubStr += cameraSubStr +=
AppLocalizations.of(context).secondCountSymbol(_exposureTime); AppLocalizations.of(context)!.secondCountSymbol(_exposureTime!);
cameraSubStr += space; cameraSubStr += space;
} }
if (_focalLength != null) { if (_focalLength != null) {
cameraSubStr += AppLocalizations.of(context) cameraSubStr += AppLocalizations.of(context)!
.millimeterCountSymbol(_focalLength.toStringAsFixedTruncated(2)); .millimeterCountSymbol(_focalLength!.toStringAsFixedTruncated(2));
cameraSubStr += space; cameraSubStr += space;
} }
if (_isoSpeedRatings != null) { if (_isoSpeedRatings != null) {
@ -112,12 +112,12 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
children: [ children: [
_DetailPaneButton( _DetailPaneButton(
icon: Icons.playlist_add_outlined, icon: Icons.playlist_add_outlined,
label: AppLocalizations.of(context).addToAlbumTooltip, label: AppLocalizations.of(context)!.addToAlbumTooltip,
onPressed: () => _onAddToAlbumPressed(context), onPressed: () => _onAddToAlbumPressed(context),
), ),
_DetailPaneButton( _DetailPaneButton(
icon: Icons.delete_outline, icon: Icons.delete_outline,
label: AppLocalizations.of(context).deleteTooltip, label: AppLocalizations.of(context)!.deleteTooltip,
onPressed: () => _onDeletePressed(context), onPressed: () => _onDeletePressed(context),
), ),
], ],
@ -154,7 +154,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
title: Text( title: Text(
"${widget.file.metadata.imageWidth} x ${widget.file.metadata.imageHeight}"), "${widget.file.metadata!.imageWidth} x ${widget.file.metadata!.imageHeight}"),
subtitle: Text(sizeSubStr), subtitle: Text(sizeSubStr),
) )
else else
@ -163,7 +163,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
Icons.aspect_ratio, Icons.aspect_ratio,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
title: Text(_byteSizeToString(widget.file.contentLength)), title: Text(_byteSizeToString(widget.file.contentLength ?? 0)),
), ),
if (_model != null) if (_model != null)
ListTile( ListTile(
@ -171,14 +171,14 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
Icons.camera_outlined, Icons.camera_outlined,
color: AppTheme.getSecondaryTextColor(context), color: AppTheme.getSecondaryTextColor(context),
), ),
title: Text(_model), title: Text(_model!),
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null, subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
), ),
if (features.isSupportMapView && _gps != null) if (features.isSupportMapView && _gps != null)
SizedBox( SizedBox(
height: 256, height: 256,
child: platform.Map( child: platform.Map(
center: _gps, center: _gps!,
zoom: 16, zoom: 16,
onTap: _onMapTap, onTap: _onMapTap,
), ),
@ -190,35 +190,35 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
/// Convert EXIF data to readable format /// Convert EXIF data to readable format
void _initMetadata() { void _initMetadata() {
final exif = widget.file.metadata.exif; final exif = widget.file.metadata!.exif!;
_log.info("[_initMetadata] $exif"); _log.info("[_initMetadata] $exif");
if (exif.make != null && exif.model != null) { if (exif.make != null && exif.model != null) {
_model = "${exif.make} ${exif.model}"; _model = "${exif.make} ${exif.model}";
} }
if (exif.fNumber != null) { if (exif.fNumber != null) {
_fNumber = exif.fNumber.toDouble(); _fNumber = exif.fNumber!.toDouble();
} }
if (exif.exposureTime != null) { if (exif.exposureTime != null) {
if (exif.exposureTime.denominator == 1) { if (exif.exposureTime!.denominator == 1) {
_exposureTime = exif.exposureTime.numerator.toString(); _exposureTime = exif.exposureTime!.numerator.toString();
} else { } else {
_exposureTime = exif.exposureTime.toString(); _exposureTime = exif.exposureTime.toString();
} }
} }
if (exif.focalLength != null) { if (exif.focalLength != null) {
_focalLength = exif.focalLength.toDouble(); _focalLength = exif.focalLength!.toDouble();
} }
if (exif.isoSpeedRatings != null) { if (exif.isoSpeedRatings != null) {
_isoSpeedRatings = exif.isoSpeedRatings; _isoSpeedRatings = exif.isoSpeedRatings!;
} }
if (exif.gpsLatitudeRef != null && if (exif.gpsLatitudeRef != null &&
exif.gpsLatitude != null && exif.gpsLatitude != null &&
exif.gpsLongitudeRef != null && exif.gpsLongitudeRef != null &&
exif.gpsLongitude != null) { exif.gpsLongitude != null) {
final lat = _gpsDmsToDouble(exif.gpsLatitude) * final lat = _gpsDmsToDouble(exif.gpsLatitude!) *
(exif.gpsLatitudeRef == "S" ? -1 : 1); (exif.gpsLatitudeRef == "S" ? -1 : 1);
final lng = _gpsDmsToDouble(exif.gpsLongitude) * final lng = _gpsDmsToDouble(exif.gpsLongitude!) *
(exif.gpsLongitudeRef == "W" ? -1 : 1); (exif.gpsLongitudeRef == "W" ? -1 : 1);
_log.fine("GPS: ($lat, $lng)"); _log.fine("GPS: ($lat, $lng)");
_gps = Tuple2(lat, lng); _gps = Tuple2(lat, lng);
@ -238,7 +238,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
_log.info("[_onAddToAlbumPressed] Album picked: ${value.name}"); _log.info("[_onAddToAlbumPressed] Album picked: ${value.name}");
_addToAlbum(context, value).then((_) { _addToAlbum(context, value).then((_) {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context) content: Text(AppLocalizations.of(context)!
.addToAlbumSuccessNotification(value.name)), .addToAlbumSuccessNotification(value.name)),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -246,7 +246,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} else { } else {
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: content:
Text(AppLocalizations.of(context).addToAlbumFailureNotification), Text(AppLocalizations.of(context)!.addToAlbumFailureNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} }
@ -255,7 +255,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
"[_onAddToAlbumPressed] Failed while showDialog", e, stacktrace); "[_onAddToAlbumPressed] Failed while showDialog", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
"${AppLocalizations.of(context).addToAlbumFailureNotification}: " "${AppLocalizations.of(context)!.addToAlbumFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -265,10 +265,10 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
void _onDeletePressed(BuildContext context) async { void _onDeletePressed(BuildContext context) async {
_log.info("[_onDeletePressed] Removing file: ${widget.file.path}"); _log.info("[_onDeletePressed] Removing file: ${widget.file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar( var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).deleteProcessingNotification), content: Text(AppLocalizations.of(context)!.deleteProcessingNotification),
duration: k.snackBarDurationShort, duration: k.snackBarDurationShort,
)); ));
controller?.closed?.whenComplete(() { controller?.closed.whenComplete(() {
controller = null; controller = null;
}); });
try { try {
@ -276,7 +276,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
AlbumRepo(AlbumCachedDataSource()))(widget.account, widget.file); AlbumRepo(AlbumCachedDataSource()))(widget.account, widget.file);
controller?.close(); controller?.close();
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).deleteSuccessNotification), content: Text(AppLocalizations.of(context)!.deleteSuccessNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -289,7 +289,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
controller?.close(); controller?.close();
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: content:
Text("${AppLocalizations.of(context).deleteFailureNotification}: " Text("${AppLocalizations.of(context)!.deleteFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -300,7 +300,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
if (platform_k.isAndroid) { if (platform_k.isAndroid) {
final intent = AndroidIntent( final intent = AndroidIntent(
action: "action_view", 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(); intent.launch();
} }
@ -329,7 +329,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
stacktrace); stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
AppLocalizations.of(context).updateDateTimeFailureNotification), AppLocalizations.of(context)!.updateDateTimeFailureNotification),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
} }
@ -362,7 +362,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
_log.info("[_addToAlbum] File already in album: ${widget.file.path}"); _log.info("[_addToAlbum] File already in album: ${widget.file.path}");
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
"${AppLocalizations.of(context).addToAlbumAlreadyAddedNotification}"), "${AppLocalizations.of(context)!.addToAlbumAlreadyAddedNotification}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
return Future.error(ArgumentError("File already in album")); 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); _log.shout("[_addToAlbum] Failed while updating album", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar( SnackBarManager().showSnackBar(SnackBar(
content: Text( content: Text(
"${AppLocalizations.of(context).addToAlbumFailureNotification}: " "${AppLocalizations.of(context)!.addToAlbumFailureNotification}: "
"${exception_util.toUserString(e, context)}"), "${exception_util.toUserString(e, context)}"),
duration: k.snackBarDurationNormal, duration: k.snackBarDurationNormal,
)); ));
@ -389,22 +389,26 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
} }
} }
DateTime _dateTime; late DateTime _dateTime;
// EXIF data // EXIF data
String _model; String? _model;
double _fNumber; double? _fNumber;
String _exposureTime; String? _exposureTime;
double _focalLength; double? _focalLength;
int _isoSpeedRatings; int? _isoSpeedRatings;
Tuple2<double, double> _gps; Tuple2<double, double>? _gps;
static final _log = static final _log =
Logger("widget.viewer_detail_pane._ViewerDetailPaneState"); Logger("widget.viewer_detail_pane._ViewerDetailPaneState");
} }
class _DetailPaneButton extends StatelessWidget { class _DetailPaneButton extends StatelessWidget {
const _DetailPaneButton({Key key, this.icon, this.label, this.onPressed}) const _DetailPaneButton({
: super(key: key); Key? key,
required this.icon,
required this.label,
required this.onPressed,
}) : super(key: key);
@override @override
build(BuildContext context) { build(BuildContext context) {
@ -439,7 +443,7 @@ class _DetailPaneButton extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final VoidCallback onPressed; final VoidCallback? onPressed;
} }
String _byteSizeToString(int byteSize) { String _byteSizeToString(int byteSize) {

View file

@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.22.0+2200 version: 1.22.0+2200
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
dependencies: dependencies:
flutter: flutter:

View file

@ -14,6 +14,7 @@ void main() {
final json = <String, dynamic>{ final json = <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{ "provider": <String, dynamic>{
"type": "static", "type": "static",
"content": <String, dynamic>{ "content": <String, dynamic>{
@ -30,7 +31,8 @@ void main() {
}, },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
@ -63,7 +65,8 @@ void main() {
}, },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "album", name: "album",
@ -114,7 +117,8 @@ void main() {
}, },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
@ -161,7 +165,8 @@ void main() {
}, },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
@ -203,7 +208,8 @@ void main() {
}, },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
@ -242,7 +248,8 @@ void main() {
}, },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",
@ -260,6 +267,7 @@ void main() {
final json = <String, dynamic>{ final json = <String, dynamic>{
"version": Album.version, "version": Album.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "lastUpdated": "2020-01-02T03:04:05.678901Z",
"name": "",
"provider": <String, dynamic>{ "provider": <String, dynamic>{
"type": "static", "type": "static",
"content": <String, dynamic>{ "content": <String, dynamic>{
@ -279,7 +287,8 @@ void main() {
}, },
}; };
expect( expect(
Album.fromJson(json), Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album( Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "", name: "",

View file

@ -109,7 +109,7 @@ void main() {
"version": Metadata.version, "version": Metadata.version,
"lastUpdated": "2020-01-02T03:04:05.678901Z", "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))); Metadata(lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901)));
}); });
@ -120,7 +120,7 @@ void main() {
"fileEtag": "8a3e0799b6f0711c23cc2d93950eceb5", "fileEtag": "8a3e0799b6f0711c23cc2d93950eceb5",
}; };
expect( expect(
Metadata.fromJson(json), Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
Metadata( Metadata(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5", fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
@ -134,7 +134,7 @@ void main() {
"imageWidth": 1024, "imageWidth": 1024,
}; };
expect( expect(
Metadata.fromJson(json), Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
Metadata( Metadata(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
imageWidth: 1024, imageWidth: 1024,
@ -148,7 +148,7 @@ void main() {
"imageHeight": 768, "imageHeight": 768,
}; };
expect( expect(
Metadata.fromJson(json), Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
Metadata( Metadata(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
imageHeight: 768, imageHeight: 768,
@ -164,7 +164,7 @@ void main() {
}, },
}; };
expect( expect(
Metadata.fromJson(json), Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
Metadata( Metadata(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901), lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
exif: Exif({ exif: Exif({