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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,10 +3,10 @@ import 'package:tuple/tuple.dart';
extension IterableExtension<T> on Iterable<T> {
/// Return a new sorted list
List<T> sorted([int compare(T a, T b)]) => this.toList()..sort(compare);
List<T> sorted([int compare(T a, T b)?]) => this.toList()..sort(compare);
/// Return a new stable sorted list
List<T> stableSorted([int compare(T a, T b)]) {
List<T> stableSorted([int compare(T a, T b)?]) {
final tmp = this.toList();
mergeSort(tmp, compare: compare);
return tmp;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,5 +11,5 @@ class AndroidItemDownloadSuccessfulNotification
}
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>[];
static SelfSignedCertManager _inst = SelfSignedCertManager._();
@ -168,7 +168,7 @@ class _BadCertInfo {
class _CustomHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext context) {
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback = (cert, host, port) {
try {

View file

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

View file

@ -2,7 +2,9 @@
class OrNull<T> {
OrNull(this.obj);
static bool isNull(OrNull x) => x != null && x.obj == null;
/// Return iff the value of [x] is set to null, which means if [x] itself is
/// null, false will still be returned
static bool isSetNull(OrNull? x) => x != null && x.obj == null;
final T obj;
final T? obj;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,9 +12,9 @@ class UpdateProperty {
Future<void> call(
Account account,
File file, {
OrNull<Metadata> metadata,
OrNull<bool> isArchived,
OrNull<DateTime> overrideDateTime,
OrNull<Metadata>? metadata,
OrNull<bool>? isArchived,
OrNull<DateTime>? overrideDateTime,
}) async {
if (metadata == null && isArchived == null && overrideDateTime == null) {
// ?
@ -22,9 +22,9 @@ class UpdateProperty {
return;
}
if (metadata?.obj != null && metadata.obj.fileEtag != file.etag) {
if (metadata?.obj != null && metadata!.obj!.fileEtag != file.etag) {
_log.warning(
"[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj.fileEtag}, file: ${file.etag})");
"[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj!.fileEtag}, file: ${file.etag})");
}
await fileRepo.updateProperty(
account,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
environment:
sdk: ">=2.10.0 <3.0.0"
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:

View file

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

View file

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