mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-04 22:38:51 +01:00
Migrate to null-safety
This commit is contained in:
parent
1a4779f465
commit
df26b7a8c8
97 changed files with 1380 additions and 1315 deletions
|
@ -18,11 +18,11 @@ class Account with EquatableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Account copyWith({
|
Account copyWith({
|
||||||
String scheme,
|
String? scheme,
|
||||||
String address,
|
String? address,
|
||||||
String username,
|
String? username,
|
||||||
String password,
|
String? password,
|
||||||
List<String> roots,
|
List<String>? roots,
|
||||||
}) {
|
}) {
|
||||||
return Account(
|
return Account(
|
||||||
scheme ?? this.scheme,
|
scheme ?? this.scheme,
|
||||||
|
@ -39,7 +39,7 @@ class Account with EquatableMixin {
|
||||||
"scheme: '$scheme', "
|
"scheme: '$scheme', "
|
||||||
"address: '$address', "
|
"address: '$address', "
|
||||||
"username: '$username', "
|
"username: '$username', "
|
||||||
"password: '${password?.isNotEmpty == true ? (kDebugMode ? password : '***') : null}', "
|
"password: '${password.isNotEmpty == true ? (kDebugMode ? password : '***') : null}', "
|
||||||
"roots: List {'${roots.join('\', \'')}'}, "
|
"roots: List {'${roots.join('\', \'')}'}, "
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
|
@ -43,9 +42,9 @@ class Api {
|
||||||
Future<Response> request(
|
Future<Response> request(
|
||||||
String method,
|
String method,
|
||||||
String endpoint, {
|
String endpoint, {
|
||||||
Map<String, String> header,
|
Map<String, String>? header,
|
||||||
String body,
|
String? body,
|
||||||
Uint8List bodyBytes,
|
Uint8List? bodyBytes,
|
||||||
bool isResponseString = true,
|
bool isResponseString = true,
|
||||||
}) async {
|
}) async {
|
||||||
final url = _makeUri(endpoint);
|
final url = _makeUri(endpoint);
|
||||||
|
@ -114,7 +113,7 @@ class _Files {
|
||||||
Api _api;
|
Api _api;
|
||||||
|
|
||||||
Future<Response> delete({
|
Future<Response> delete({
|
||||||
@required String path,
|
required String path,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
return await _api.request("DELETE", path);
|
return await _api.request("DELETE", path);
|
||||||
|
@ -125,7 +124,7 @@ class _Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> get({
|
Future<Response> get({
|
||||||
@required String path,
|
required String path,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
return await _api.request("GET", path, isResponseString: false);
|
return await _api.request("GET", path, isResponseString: false);
|
||||||
|
@ -136,9 +135,9 @@ class _Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> put({
|
Future<Response> put({
|
||||||
@required String path,
|
required String path,
|
||||||
String mime = "application/octet-stream",
|
String mime = "application/octet-stream",
|
||||||
Uint8List content,
|
required Uint8List content,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
return await _api.request(
|
return await _api.request(
|
||||||
|
@ -156,8 +155,8 @@ class _Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> propfind({
|
Future<Response> propfind({
|
||||||
@required String path,
|
required String path,
|
||||||
int depth,
|
int? depth,
|
||||||
getlastmodified,
|
getlastmodified,
|
||||||
getetag,
|
getetag,
|
||||||
getcontenttype,
|
getcontenttype,
|
||||||
|
@ -176,8 +175,8 @@ class _Files {
|
||||||
hasPreview,
|
hasPreview,
|
||||||
size,
|
size,
|
||||||
richWorkspace,
|
richWorkspace,
|
||||||
Map<String, String> customNamespaces,
|
Map<String, String>? customNamespaces,
|
||||||
List<String> customProperties,
|
List<String>? customProperties,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final bool hasDavNs = (getlastmodified != null ||
|
final bool hasDavNs = (getlastmodified != null ||
|
||||||
|
@ -287,10 +286,10 @@ class _Files {
|
||||||
/// [namespaces] should be specified in the format {"URI": "prefix"}, eg,
|
/// [namespaces] should be specified in the format {"URI": "prefix"}, eg,
|
||||||
/// {"DAV:": "d"}
|
/// {"DAV:": "d"}
|
||||||
Future<Response> proppatch({
|
Future<Response> proppatch({
|
||||||
@required String path,
|
required String path,
|
||||||
Map<String, String> namespaces,
|
Map<String, String>? namespaces,
|
||||||
Map<String, dynamic> set,
|
Map<String, dynamic>? set,
|
||||||
List<String> remove,
|
List<String>? remove,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final ns = <String, String>{
|
final ns = <String, String>{
|
||||||
|
@ -300,7 +299,7 @@ class _Files {
|
||||||
builder
|
builder
|
||||||
..processing("xml", "version=\"1.0\"")
|
..processing("xml", "version=\"1.0\"")
|
||||||
..element("d:propertyupdate", namespaces: ns, nest: () {
|
..element("d:propertyupdate", namespaces: ns, nest: () {
|
||||||
if (set?.isNotEmpty == true) {
|
if (set != null && set.isNotEmpty) {
|
||||||
builder.element("d:set", nest: () {
|
builder.element("d:set", nest: () {
|
||||||
builder.element("d:prop", nest: () {
|
builder.element("d:prop", nest: () {
|
||||||
for (final e in set.entries) {
|
for (final e in set.entries) {
|
||||||
|
@ -311,7 +310,7 @@ class _Files {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (remove?.isNotEmpty == true) {
|
if (remove != null && remove.isNotEmpty) {
|
||||||
builder.element("d:remove", nest: () {
|
builder.element("d:remove", nest: () {
|
||||||
builder.element("d:prop", nest: () {
|
builder.element("d:prop", nest: () {
|
||||||
for (final e in remove) {
|
for (final e in remove) {
|
||||||
|
@ -331,7 +330,7 @@ class _Files {
|
||||||
|
|
||||||
/// A folder can be created by sending a MKCOL request to the folder
|
/// A folder can be created by sending a MKCOL request to the folder
|
||||||
Future<Response> mkcol({
|
Future<Response> mkcol({
|
||||||
@required String path,
|
required String path,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
return await _api.request("MKCOL", path);
|
return await _api.request("MKCOL", path);
|
||||||
|
@ -344,9 +343,9 @@ class _Files {
|
||||||
/// A file or folder can be copied by sending a COPY request to the file or
|
/// A file or folder can be copied by sending a COPY request to the file or
|
||||||
/// folder and specifying the [destinationUrl] as full url
|
/// folder and specifying the [destinationUrl] as full url
|
||||||
Future<Response> copy({
|
Future<Response> copy({
|
||||||
@required String path,
|
required String path,
|
||||||
@required String destinationUrl,
|
required String destinationUrl,
|
||||||
bool overwrite,
|
bool? overwrite,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
return await _api.request("COPY", path, header: {
|
return await _api.request("COPY", path, header: {
|
||||||
|
@ -362,9 +361,9 @@ class _Files {
|
||||||
/// A file or folder can be moved by sending a MOVE request to the file or
|
/// A file or folder can be moved by sending a MOVE request to the file or
|
||||||
/// folder and specifying the [destinationUrl] as full url
|
/// folder and specifying the [destinationUrl] as full url
|
||||||
Future<Response> move({
|
Future<Response> move({
|
||||||
@required String path,
|
required String path,
|
||||||
@required String destinationUrl,
|
required String destinationUrl,
|
||||||
bool overwrite,
|
bool? overwrite,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
return await _api.request("MOVE", path, header: {
|
return await _api.request("MOVE", path, header: {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/// Helper functions working with remote Nextcloud server
|
/// Helper functions working with remote Nextcloud server
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/api/api.dart';
|
import 'package:nc_photos/api/api.dart';
|
||||||
|
@ -10,10 +9,10 @@ import 'package:nc_photos/exception.dart';
|
||||||
String getFilePreviewUrl(
|
String getFilePreviewUrl(
|
||||||
Account account,
|
Account account,
|
||||||
File file, {
|
File file, {
|
||||||
@required int width,
|
required int width,
|
||||||
@required int height,
|
required int height,
|
||||||
String mode,
|
String? mode,
|
||||||
bool a,
|
bool? a,
|
||||||
}) {
|
}) {
|
||||||
return "${account.url}/"
|
return "${account.url}/"
|
||||||
"${getFilePreviewUrlRelative(file, width: width, height: height, mode: mode, a: a)}";
|
"${getFilePreviewUrlRelative(file, width: width, height: height, mode: mode, a: a)}";
|
||||||
|
@ -24,10 +23,10 @@ String getFilePreviewUrl(
|
||||||
/// cropped
|
/// cropped
|
||||||
String getFilePreviewUrlRelative(
|
String getFilePreviewUrlRelative(
|
||||||
File file, {
|
File file, {
|
||||||
@required int width,
|
required int width,
|
||||||
@required int height,
|
required int height,
|
||||||
String mode,
|
String? mode,
|
||||||
bool a,
|
bool? a,
|
||||||
}) {
|
}) {
|
||||||
String url;
|
String url;
|
||||||
if (file.fileId != null) {
|
if (file.fileId != null) {
|
||||||
|
@ -70,7 +69,7 @@ Future<String> exchangePassword(Account account) async {
|
||||||
try {
|
try {
|
||||||
final appPwdRegex = RegExp(r"<apppassword>(.*)</apppassword>");
|
final appPwdRegex = RegExp(r"<apppassword>(.*)</apppassword>");
|
||||||
final appPwdMatch = appPwdRegex.firstMatch(response.body);
|
final appPwdMatch = appPwdRegex.firstMatch(response.body);
|
||||||
return appPwdMatch.group(1);
|
return appPwdMatch!.group(1)!;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// this happens when the address is not the base URL and so Nextcloud
|
// this happens when the address is not the base URL and so Nextcloud
|
||||||
// returned the login page
|
// returned the login page
|
||||||
|
|
|
@ -8,7 +8,6 @@ import 'package:nc_photos/entity/album/upgrader.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/mobile/platform.dart'
|
import 'package:nc_photos/mobile/platform.dart'
|
||||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||||
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
|
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
class AppDb {
|
class AppDb {
|
||||||
|
@ -140,7 +139,7 @@ class AppDbAlbumEntry {
|
||||||
upgraderV1: AlbumUpgraderV1(),
|
upgraderV1: AlbumUpgraderV1(),
|
||||||
upgraderV2: AlbumUpgraderV2(),
|
upgraderV2: AlbumUpgraderV2(),
|
||||||
upgraderV3: AlbumUpgraderV3(),
|
upgraderV3: AlbumUpgraderV3(),
|
||||||
),
|
)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,12 +91,12 @@ class AlbumSearchBloc extends Bloc<AlbumSearchBlocEvent, AlbumSearchBlocState> {
|
||||||
_albums = ev.albums;
|
_albums = ev.albums;
|
||||||
if (_lastSearch != null) {
|
if (_lastSearch != null) {
|
||||||
// search again
|
// search again
|
||||||
yield* _onEventSearch(_lastSearch);
|
yield* _onEventSearch(_lastSearch!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _albums = <Album>[];
|
var _albums = <Album>[];
|
||||||
AlbumSearchBlocSearchEvent _lastSearch;
|
AlbumSearchBlocSearchEvent? _lastSearch;
|
||||||
|
|
||||||
static final _log = Logger("bloc.album_search.AlbumSearchBloc");
|
static final _log = Logger("bloc.album_search.AlbumSearchBloc");
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,12 +112,12 @@ class AlbumSearchSuggestionBloc extends Bloc<AlbumSearchSuggestionBlocEvent,
|
||||||
}
|
}
|
||||||
if (_lastSearch != null) {
|
if (_lastSearch != null) {
|
||||||
// search again
|
// search again
|
||||||
yield* _onEventSearch(_lastSearch);
|
yield* _onEventSearch(_lastSearch!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final _search = Woozy(limit: 5);
|
final _search = Woozy(limit: 5);
|
||||||
AlbumSearchSuggestionBlocSearchEvent _lastSearch;
|
AlbumSearchSuggestionBlocSearchEvent? _lastSearch;
|
||||||
|
|
||||||
static final _log =
|
static final _log =
|
||||||
Logger("bloc.album_search_suggestion.AlbumSearchSuggestionBloc");
|
Logger("bloc.album_search_suggestion.AlbumSearchSuggestionBloc");
|
||||||
|
|
|
@ -47,7 +47,7 @@ abstract class ListAlbumBlocState {
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account account;
|
final Account? account;
|
||||||
final List<Album> albums;
|
final List<Album> albums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,18 +56,18 @@ class ListAlbumBlocInit extends ListAlbumBlocState {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListAlbumBlocLoading extends ListAlbumBlocState {
|
class ListAlbumBlocLoading extends ListAlbumBlocState {
|
||||||
const ListAlbumBlocLoading(Account account, List<Album> albums)
|
const ListAlbumBlocLoading(Account? account, List<Album> albums)
|
||||||
: super(account, albums);
|
: super(account, albums);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListAlbumBlocSuccess extends ListAlbumBlocState {
|
class ListAlbumBlocSuccess extends ListAlbumBlocState {
|
||||||
const ListAlbumBlocSuccess(Account account, List<Album> albums)
|
const ListAlbumBlocSuccess(Account? account, List<Album> albums)
|
||||||
: super(account, albums);
|
: super(account, albums);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListAlbumBlocFailure extends ListAlbumBlocState {
|
class ListAlbumBlocFailure extends ListAlbumBlocState {
|
||||||
const ListAlbumBlocFailure(
|
const ListAlbumBlocFailure(
|
||||||
Account account, List<Album> albums, this.exception)
|
Account? account, List<Album> albums, this.exception)
|
||||||
: super(account, albums);
|
: super(account, albums);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -84,7 +84,7 @@ class ListAlbumBlocFailure extends ListAlbumBlocState {
|
||||||
/// The state of this bloc is inconsistent. This typically means that the data
|
/// The state of this bloc is inconsistent. This typically means that the data
|
||||||
/// may have been changed externally
|
/// may have been changed externally
|
||||||
class ListAlbumBlocInconsistent extends ListAlbumBlocState {
|
class ListAlbumBlocInconsistent extends ListAlbumBlocState {
|
||||||
const ListAlbumBlocInconsistent(Account account, List<Album> albums)
|
const ListAlbumBlocInconsistent(Account? account, List<Album> albums)
|
||||||
: super(account, albums);
|
: super(account, albums);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
|
||||||
if (newState is ListAlbumBlocFailure) {
|
if (newState is ListAlbumBlocFailure) {
|
||||||
yield ListAlbumBlocFailure(
|
yield ListAlbumBlocFailure(
|
||||||
ev.account,
|
ev.account,
|
||||||
newState.albums?.isNotEmpty == true ? newState.albums : state.albums,
|
newState.albums.isNotEmpty ? newState.albums : state.albums,
|
||||||
newState.exception);
|
newState.exception);
|
||||||
} else {
|
} else {
|
||||||
yield newState;
|
yield newState;
|
||||||
|
@ -206,9 +206,9 @@ class ListAlbumBloc extends Bloc<ListAlbumBlocEvent, ListAlbumBlocState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
|
late AppEventListener<AlbumUpdatedEvent> _albumUpdatedListener;
|
||||||
AppEventListener<FileRemovedEvent> _fileRemovedListener;
|
late AppEventListener<FileRemovedEvent> _fileRemovedListener;
|
||||||
AppEventListener<AlbumCreatedEvent> _albumCreatedListener;
|
late AppEventListener<AlbumCreatedEvent> _albumCreatedListener;
|
||||||
|
|
||||||
static final _log = Logger("bloc.list_album.ListAlbumBloc");
|
static final _log = Logger("bloc.list_album.ListAlbumBloc");
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,11 @@ class LsDirBlocItem {
|
||||||
if (isDeep) {
|
if (isDeep) {
|
||||||
return "$runtimeType:${_toDeepString(0)}";
|
return "$runtimeType:${_toDeepString(0)}";
|
||||||
} else {
|
} else {
|
||||||
|
final childrenStr =
|
||||||
|
children == null ? "null" : "List {length: ${children!.length}}";
|
||||||
return "$runtimeType {"
|
return "$runtimeType {"
|
||||||
"file: '${file.path}', "
|
"file: '${file.path}', "
|
||||||
"children: List {length: ${children.length}}, "
|
"children: $childrenStr, "
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,19 +25,19 @@ class LsDirBlocItem {
|
||||||
String _toDeepString(int level) {
|
String _toDeepString(int level) {
|
||||||
String product = "\n" + " " * (level * 2) + "-${file.path}";
|
String product = "\n" + " " * (level * 2) + "-${file.path}";
|
||||||
if (children != null) {
|
if (children != null) {
|
||||||
for (final c in children) {
|
for (final c in children!) {
|
||||||
product += c._toDeepString(level + 1);
|
product += c._toDeepString(level + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
|
|
||||||
File file;
|
final File file;
|
||||||
|
|
||||||
/// Child directories under this directory
|
/// Child directories under this directory
|
||||||
///
|
///
|
||||||
/// Null if this dir is not listed, due to things like depth limitation
|
/// Null if this dir is not listed, due to things like depth limitation
|
||||||
List<LsDirBlocItem> children;
|
List<LsDirBlocItem>? children;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class LsDirBlocEvent {
|
abstract class LsDirBlocEvent {
|
||||||
|
@ -59,9 +61,9 @@ class LsDirBlocQuery extends LsDirBlocEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
LsDirBlocQuery copyWith({
|
LsDirBlocQuery copyWith({
|
||||||
Account account,
|
Account? account,
|
||||||
File root,
|
File? root,
|
||||||
int depth,
|
int? depth,
|
||||||
}) {
|
}) {
|
||||||
return LsDirBlocQuery(
|
return LsDirBlocQuery(
|
||||||
account ?? this.account,
|
account ?? this.account,
|
||||||
|
@ -76,11 +78,7 @@ class LsDirBlocQuery extends LsDirBlocEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class LsDirBlocState {
|
abstract class LsDirBlocState {
|
||||||
const LsDirBlocState(this._account, this._root, this._items);
|
const LsDirBlocState(this.account, this.root, this.items);
|
||||||
|
|
||||||
Account get account => _account;
|
|
||||||
File get root => _root;
|
|
||||||
List<LsDirBlocItem> get items => _items;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() {
|
toString() {
|
||||||
|
@ -91,9 +89,9 @@ abstract class LsDirBlocState {
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account _account;
|
final Account? account;
|
||||||
final File _root;
|
final File root;
|
||||||
final List<LsDirBlocItem> _items;
|
final List<LsDirBlocItem> items;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LsDirBlocInit extends LsDirBlocState {
|
class LsDirBlocInit extends LsDirBlocState {
|
||||||
|
@ -101,18 +99,18 @@ class LsDirBlocInit extends LsDirBlocState {
|
||||||
}
|
}
|
||||||
|
|
||||||
class LsDirBlocLoading extends LsDirBlocState {
|
class LsDirBlocLoading extends LsDirBlocState {
|
||||||
const LsDirBlocLoading(Account account, File root, List<LsDirBlocItem> items)
|
const LsDirBlocLoading(Account? account, File root, List<LsDirBlocItem> items)
|
||||||
: super(account, root, items);
|
: super(account, root, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LsDirBlocSuccess extends LsDirBlocState {
|
class LsDirBlocSuccess extends LsDirBlocState {
|
||||||
const LsDirBlocSuccess(Account account, File root, List<LsDirBlocItem> items)
|
const LsDirBlocSuccess(Account? account, File root, List<LsDirBlocItem> items)
|
||||||
: super(account, root, items);
|
: super(account, root, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LsDirBlocFailure extends LsDirBlocState {
|
class LsDirBlocFailure extends LsDirBlocState {
|
||||||
const LsDirBlocFailure(
|
const LsDirBlocFailure(
|
||||||
Account account, File root, List<LsDirBlocItem> items, this.exception)
|
Account? account, File root, List<LsDirBlocItem> items, this.exception)
|
||||||
: super(account, root, items);
|
: super(account, root, items);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -153,12 +151,12 @@ class LsDirBloc extends Bloc<LsDirBlocEvent, LsDirBlocState> {
|
||||||
var files = _cache[ev.root.path];
|
var files = _cache[ev.root.path];
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
files = (await Ls(FileRepo(FileWebdavDataSource()))(ev.account, ev.root))
|
files = (await Ls(FileRepo(FileWebdavDataSource()))(ev.account, ev.root))
|
||||||
.where((f) => f.isCollection)
|
.where((f) => f.isCollection ?? false)
|
||||||
.toList();
|
.toList();
|
||||||
_cache[ev.root.path] = files;
|
_cache[ev.root.path] = files;
|
||||||
}
|
}
|
||||||
for (final f in files) {
|
for (final f in files) {
|
||||||
List<LsDirBlocItem> children;
|
List<LsDirBlocItem>? children;
|
||||||
if (ev.depth > 1) {
|
if (ev.depth > 1) {
|
||||||
children = await _query(ev.copyWith(root: f, depth: ev.depth - 1));
|
children = await _query(ev.copyWith(root: f, depth: ev.depth - 1));
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,10 +58,7 @@ class _ScanDirBlocExternalEvent extends ScanDirBlocEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class ScanDirBlocState {
|
abstract class ScanDirBlocState {
|
||||||
const ScanDirBlocState(this._account, this._files);
|
const ScanDirBlocState(this.account, this.files);
|
||||||
|
|
||||||
Account get account => _account;
|
|
||||||
List<File> get files => _files;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() {
|
toString() {
|
||||||
|
@ -71,8 +68,8 @@ abstract class ScanDirBlocState {
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account _account;
|
final Account? account;
|
||||||
final List<File> _files;
|
final List<File> files;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanDirBlocInit extends ScanDirBlocState {
|
class ScanDirBlocInit extends ScanDirBlocState {
|
||||||
|
@ -80,17 +77,17 @@ class ScanDirBlocInit extends ScanDirBlocState {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanDirBlocLoading extends ScanDirBlocState {
|
class ScanDirBlocLoading extends ScanDirBlocState {
|
||||||
const ScanDirBlocLoading(Account account, List<File> files)
|
const ScanDirBlocLoading(Account? account, List<File> files)
|
||||||
: super(account, files);
|
: super(account, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanDirBlocSuccess extends ScanDirBlocState {
|
class ScanDirBlocSuccess extends ScanDirBlocState {
|
||||||
const ScanDirBlocSuccess(Account account, List<File> files)
|
const ScanDirBlocSuccess(Account? account, List<File> files)
|
||||||
: super(account, files);
|
: super(account, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanDirBlocFailure extends ScanDirBlocState {
|
class ScanDirBlocFailure extends ScanDirBlocState {
|
||||||
const ScanDirBlocFailure(Account account, List<File> files, this.exception)
|
const ScanDirBlocFailure(Account? account, List<File> files, this.exception)
|
||||||
: super(account, files);
|
: super(account, files);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -107,7 +104,7 @@ class ScanDirBlocFailure extends ScanDirBlocState {
|
||||||
/// The state of this bloc is inconsistent. This typically means that the data
|
/// The state of this bloc is inconsistent. This typically means that the data
|
||||||
/// may have been changed externally
|
/// may have been changed externally
|
||||||
class ScanDirBlocInconsistent extends ScanDirBlocState {
|
class ScanDirBlocInconsistent extends ScanDirBlocState {
|
||||||
const ScanDirBlocInconsistent(Account account, List<File> files)
|
const ScanDirBlocInconsistent(Account? account, List<File> files)
|
||||||
: super(account, files);
|
: super(account, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,11 +282,12 @@ class ScanDirBloc extends Bloc<ScanDirBlocEvent, ScanDirBlocState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppEventListener<FileRemovedEvent> _fileRemovedEventListener;
|
late AppEventListener<FileRemovedEvent> _fileRemovedEventListener;
|
||||||
AppEventListener<FilePropertyUpdatedEvent> _filePropertyUpdatedEventListener;
|
late AppEventListener<FilePropertyUpdatedEvent>
|
||||||
|
_filePropertyUpdatedEventListener;
|
||||||
|
|
||||||
int _successivePropertyUpdatedCount = 0;
|
int _successivePropertyUpdatedCount = 0;
|
||||||
StreamSubscription<void> _propertyUpdatedSubscription;
|
StreamSubscription<void>? _propertyUpdatedSubscription;
|
||||||
|
|
||||||
bool _shouldCheckCache = true;
|
bool _shouldCheckCache = true;
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,16 @@ class CancelableGetFile {
|
||||||
|
|
||||||
Future<FileInfo> getFileUntil(String key,
|
Future<FileInfo> getFileUntil(String key,
|
||||||
{bool ignoreMemCache = false}) async {
|
{bool ignoreMemCache = false}) async {
|
||||||
FileInfo product;
|
FileInfo? product;
|
||||||
while (product == null && _shouldRun) {
|
while (product == null && _shouldRun) {
|
||||||
product = await store.getFile(key, ignoreMemCache: ignoreMemCache);
|
product = await store.getFile(key, ignoreMemCache: ignoreMemCache);
|
||||||
await Future.delayed(Duration(milliseconds: 500));
|
await Future.delayed(Duration(milliseconds: 500));
|
||||||
}
|
}
|
||||||
return product ?? Future.error("Interrupted");
|
if (product == null) {
|
||||||
|
return Future.error("Interrupted");
|
||||||
|
} else {
|
||||||
|
return product;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cancel() {
|
void cancel() {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:idb_sqflite/idb_sqflite.dart';
|
import 'package:idb_sqflite/idb_sqflite.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
|
@ -31,57 +30,57 @@ bool isAlbumFile(Account account, File file) =>
|
||||||
/// Immutable object that represents an album
|
/// Immutable object that represents an album
|
||||||
class Album with EquatableMixin {
|
class Album with EquatableMixin {
|
||||||
Album({
|
Album({
|
||||||
DateTime lastUpdated,
|
DateTime? lastUpdated,
|
||||||
@required String name,
|
required this.name,
|
||||||
@required this.provider,
|
required this.provider,
|
||||||
@required this.coverProvider,
|
required this.coverProvider,
|
||||||
@required this.sortProvider,
|
required this.sortProvider,
|
||||||
this.albumFile,
|
this.albumFile,
|
||||||
}) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc(),
|
}) : this.lastUpdated = (lastUpdated ?? DateTime.now()).toUtc();
|
||||||
this.name = name ?? "";
|
|
||||||
|
|
||||||
factory Album.fromJson(
|
static Album? fromJson(
|
||||||
Map<String, dynamic> json, {
|
Map<String, dynamic> json, {
|
||||||
AlbumUpgraderV1 upgraderV1,
|
required AlbumUpgraderV1? upgraderV1,
|
||||||
AlbumUpgraderV2 upgraderV2,
|
required AlbumUpgraderV2? upgraderV2,
|
||||||
AlbumUpgraderV3 upgraderV3,
|
required AlbumUpgraderV3? upgraderV3,
|
||||||
}) {
|
}) {
|
||||||
final jsonVersion = json["version"];
|
final jsonVersion = json["version"];
|
||||||
|
Map<String, dynamic>? result = json;
|
||||||
if (jsonVersion < 2) {
|
if (jsonVersion < 2) {
|
||||||
json = upgraderV1?.call(json);
|
result = upgraderV1?.call(result);
|
||||||
if (json == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 3) {
|
if (jsonVersion < 3) {
|
||||||
json = upgraderV2?.call(json);
|
result = upgraderV2?.call(result);
|
||||||
if (json == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 4) {
|
if (jsonVersion < 4) {
|
||||||
json = upgraderV3?.call(json);
|
result = upgraderV3?.call(result);
|
||||||
if (json == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Album(
|
return Album(
|
||||||
lastUpdated: json["lastUpdated"] == null
|
lastUpdated: result["lastUpdated"] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json["lastUpdated"]),
|
: DateTime.parse(result["lastUpdated"]),
|
||||||
name: json["name"],
|
name: result["name"],
|
||||||
provider:
|
provider:
|
||||||
AlbumProvider.fromJson(json["provider"].cast<String, dynamic>()),
|
AlbumProvider.fromJson(result["provider"].cast<String, dynamic>()),
|
||||||
coverProvider: AlbumCoverProvider.fromJson(
|
coverProvider: AlbumCoverProvider.fromJson(
|
||||||
json["coverProvider"].cast<String, dynamic>()),
|
result["coverProvider"].cast<String, dynamic>()),
|
||||||
sortProvider: AlbumSortProvider.fromJson(
|
sortProvider: AlbumSortProvider.fromJson(
|
||||||
json["sortProvider"].cast<String, dynamic>()),
|
result["sortProvider"].cast<String, dynamic>()),
|
||||||
albumFile: json["albumFile"] == null
|
albumFile: result["albumFile"] == null
|
||||||
? null
|
? null
|
||||||
: File.fromJson(json["albumFile"].cast<String, dynamic>()),
|
: File.fromJson(result["albumFile"].cast<String, dynamic>()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,12 +102,12 @@ class Album with EquatableMixin {
|
||||||
/// will be used. In order to keep [lastUpdated], you must explicitly assign
|
/// will be used. In order to keep [lastUpdated], you must explicitly assign
|
||||||
/// it with value from this or a null value
|
/// it with value from this or a null value
|
||||||
Album copyWith({
|
Album copyWith({
|
||||||
OrNull<DateTime> lastUpdated,
|
OrNull<DateTime>? lastUpdated,
|
||||||
String name,
|
String? name,
|
||||||
AlbumProvider provider,
|
AlbumProvider? provider,
|
||||||
AlbumCoverProvider coverProvider,
|
AlbumCoverProvider? coverProvider,
|
||||||
AlbumSortProvider sortProvider,
|
AlbumSortProvider? sortProvider,
|
||||||
File albumFile,
|
File? albumFile,
|
||||||
}) {
|
}) {
|
||||||
return Album(
|
return Album(
|
||||||
lastUpdated:
|
lastUpdated:
|
||||||
|
@ -141,7 +140,7 @@ class Album with EquatableMixin {
|
||||||
"provider": provider.toJson(),
|
"provider": provider.toJson(),
|
||||||
"coverProvider": coverProvider.toJson(),
|
"coverProvider": coverProvider.toJson(),
|
||||||
"sortProvider": sortProvider.toJson(),
|
"sortProvider": sortProvider.toJson(),
|
||||||
if (albumFile != null) "albumFile": albumFile.toJson(),
|
if (albumFile != null) "albumFile": albumFile!.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +164,7 @@ class Album with EquatableMixin {
|
||||||
/// How is this album stored on server
|
/// How is this album stored on server
|
||||||
///
|
///
|
||||||
/// This field is typically only meaningful when returned by [AlbumRepo.get]
|
/// This field is typically only meaningful when returned by [AlbumRepo.get]
|
||||||
final File albumFile;
|
final File? albumFile;
|
||||||
|
|
||||||
/// versioning of this class, use to upgrade old persisted album
|
/// versioning of this class, use to upgrade old persisted album
|
||||||
static const version = 4;
|
static const version = 4;
|
||||||
|
@ -224,7 +223,8 @@ class AlbumRemoteDataSource implements AlbumDataSource {
|
||||||
upgraderV1: AlbumUpgraderV1(),
|
upgraderV1: AlbumUpgraderV1(),
|
||||||
upgraderV2: AlbumUpgraderV2(),
|
upgraderV2: AlbumUpgraderV2(),
|
||||||
upgraderV3: AlbumUpgraderV3(),
|
upgraderV3: AlbumUpgraderV3(),
|
||||||
).copyWith(
|
)!
|
||||||
|
.copyWith(
|
||||||
lastUpdated: OrNull(null),
|
lastUpdated: OrNull(null),
|
||||||
albumFile: albumFile,
|
albumFile: albumFile,
|
||||||
);
|
);
|
||||||
|
@ -245,8 +245,8 @@ class AlbumRemoteDataSource implements AlbumDataSource {
|
||||||
final filePath =
|
final filePath =
|
||||||
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
"${remote_storage_util.getRemoteAlbumsDir(account)}/$fileName";
|
||||||
final fileRepo = FileRepo(FileWebdavDataSource());
|
final fileRepo = FileRepo(FileWebdavDataSource());
|
||||||
await PutFileBinary(fileRepo)(
|
await PutFileBinary(fileRepo)(account, filePath,
|
||||||
account, filePath, utf8.encode(jsonEncode(album.toRemoteJson())),
|
Utf8Encoder().convert(jsonEncode(album.toRemoteJson())),
|
||||||
shouldCreateMissingDir: true);
|
shouldCreateMissingDir: true);
|
||||||
// query album file
|
// query album file
|
||||||
final list = await Ls(fileRepo)(account, File(path: filePath),
|
final list = await Ls(fileRepo)(account, File(path: filePath),
|
||||||
|
@ -256,10 +256,10 @@ class AlbumRemoteDataSource implements AlbumDataSource {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
update(Account account, Album album) async {
|
update(Account account, Album album) async {
|
||||||
_log.info("[update] ${album.albumFile.path}");
|
_log.info("[update] ${album.albumFile!.path}");
|
||||||
final fileRepo = FileRepo(FileWebdavDataSource());
|
final fileRepo = FileRepo(FileWebdavDataSource());
|
||||||
await PutFileBinary(fileRepo)(account, album.albumFile.path,
|
await PutFileBinary(fileRepo)(account, album.albumFile!.path,
|
||||||
utf8.encode(jsonEncode(album.toRemoteJson())));
|
Utf8Encoder().convert(jsonEncode(album.toRemoteJson())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -286,7 +286,7 @@ class AlbumAppDbDataSource implements AlbumDataSource {
|
||||||
final path = AppDbAlbumEntry.toPathFromFile(account, albumFile);
|
final path = AppDbAlbumEntry.toPathFromFile(account, albumFile);
|
||||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||||
final List results = await index.getAll(range);
|
final List results = await index.getAll(range);
|
||||||
if (results?.isNotEmpty == true) {
|
if (results.isNotEmpty == true) {
|
||||||
final entries = results
|
final entries = results
|
||||||
.map((e) => AppDbAlbumEntry.fromJson(e.cast<String, dynamic>()));
|
.map((e) => AppDbAlbumEntry.fromJson(e.cast<String, dynamic>()));
|
||||||
if (entries.length > 1) {
|
if (entries.length > 1) {
|
||||||
|
@ -317,7 +317,7 @@ class AlbumAppDbDataSource implements AlbumDataSource {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
update(Account account, Album album) {
|
update(Account account, Album album) {
|
||||||
_log.info("[update] ${album.albumFile.path}");
|
_log.info("[update] ${album.albumFile!.path}");
|
||||||
return AppDb.use((db) async {
|
return AppDb.use((db) async {
|
||||||
final transaction =
|
final transaction =
|
||||||
db.transaction(AppDb.albumStoreName, idbModeReadWrite);
|
db.transaction(AppDb.albumStoreName, idbModeReadWrite);
|
||||||
|
@ -337,8 +337,8 @@ class AlbumCachedDataSource implements AlbumDataSource {
|
||||||
get(Account account, File albumFile) async {
|
get(Account account, File albumFile) async {
|
||||||
try {
|
try {
|
||||||
final cache = await _appDbSrc.get(account, albumFile);
|
final cache = await _appDbSrc.get(account, albumFile);
|
||||||
if (cache.albumFile.etag?.isNotEmpty == true &&
|
if (cache.albumFile!.etag?.isNotEmpty == true &&
|
||||||
cache.albumFile.etag == albumFile.etag) {
|
cache.albumFile!.etag == albumFile.etag) {
|
||||||
// cache is good
|
// cache is good
|
||||||
_log.fine(
|
_log.fine(
|
||||||
"[get] etag matched for ${AppDbAlbumEntry.toPathFromFile(account, albumFile)}");
|
"[get] etag matched for ${AppDbAlbumEntry.toPathFromFile(account, albumFile)}");
|
||||||
|
@ -412,7 +412,7 @@ class AlbumCachedDataSource implements AlbumDataSource {
|
||||||
Future<void> _cacheAlbum(
|
Future<void> _cacheAlbum(
|
||||||
ObjectStore store, Account account, Album album) async {
|
ObjectStore store, Account account, Album album) async {
|
||||||
final index = store.index(AppDbAlbumEntry.indexName);
|
final index = store.index(AppDbAlbumEntry.indexName);
|
||||||
final path = AppDbAlbumEntry.toPathFromFile(account, album.albumFile);
|
final path = AppDbAlbumEntry.toPathFromFile(account, album.albumFile!);
|
||||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||||
// count number of entries for this album
|
// count number of entries for this album
|
||||||
final count = await index.count(range);
|
final count = await index.count(range);
|
||||||
|
@ -441,7 +441,7 @@ Future<void> _cacheAlbum(
|
||||||
for (final e in entries) {
|
for (final e in entries) {
|
||||||
_log.info("[_cacheAlbum] Caching ${e.path}[${e.index}]");
|
_log.info("[_cacheAlbum] Caching ${e.path}[${e.index}]");
|
||||||
await store.put(e.toJson(),
|
await store.put(e.toJson(),
|
||||||
AppDbAlbumEntry.toPrimaryKey(account, e.album.albumFile, e.index));
|
AppDbAlbumEntry.toPrimaryKey(account, e.album.albumFile!, e.index));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count > entries.length) {
|
if (count > entries.length) {
|
||||||
|
|
|
@ -40,7 +40,7 @@ abstract class AlbumCoverProvider with EquatableMixin {
|
||||||
@override
|
@override
|
||||||
toString();
|
toString();
|
||||||
|
|
||||||
File getCover(Album album);
|
File? getCover(Album album);
|
||||||
|
|
||||||
Map<String, dynamic> _toContentJson();
|
Map<String, dynamic> _toContentJson();
|
||||||
|
|
||||||
|
@ -68,7 +68,8 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
File getCover(Album album) {
|
@override
|
||||||
|
getCover(Album album) {
|
||||||
if (coverFile == null) {
|
if (coverFile == null) {
|
||||||
try {
|
try {
|
||||||
// use the latest file as cover
|
// use the latest file as cover
|
||||||
|
@ -77,7 +78,8 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||||
.whereType<AlbumFileItem>()
|
.whereType<AlbumFileItem>()
|
||||||
.map((e) => e.file)
|
.map((e) => e.file)
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
file_util.isSupportedFormat(element) && element.hasPreview)
|
file_util.isSupportedFormat(element) &&
|
||||||
|
(element.hasPreview ?? false))
|
||||||
.sorted(compareFileDateTimeDescending)
|
.sorted(compareFileDateTimeDescending)
|
||||||
.first;
|
.first;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
@ -96,11 +98,11 @@ class AlbumAutoCoverProvider extends AlbumCoverProvider {
|
||||||
@override
|
@override
|
||||||
_toContentJson() {
|
_toContentJson() {
|
||||||
return {
|
return {
|
||||||
if (coverFile != null) "coverFile": coverFile.toJson(),
|
if (coverFile != null) "coverFile": coverFile!.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
final File coverFile;
|
final File? coverFile;
|
||||||
|
|
||||||
static const _type = "auto";
|
static const _type = "auto";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/list_extension.dart';
|
import 'package:nc_photos/list_extension.dart';
|
||||||
|
@ -60,18 +59,16 @@ abstract class AlbumItem {
|
||||||
|
|
||||||
class AlbumFileItem extends AlbumItem with EquatableMixin {
|
class AlbumFileItem extends AlbumItem with EquatableMixin {
|
||||||
AlbumFileItem({
|
AlbumFileItem({
|
||||||
@required this.file,
|
required this.file,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: hash_and_equals
|
// ignore: hash_and_equals
|
||||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||||
|
|
||||||
bool equals(Object other, {bool isDeep = false}) {
|
bool equals(Object? other, {bool isDeep = false}) {
|
||||||
if (other is AlbumFileItem) {
|
if (other is AlbumFileItem) {
|
||||||
return super == other &&
|
return super == other && (file.equals(other.file, isDeep: isDeep));
|
||||||
(file == null) == (other.file == null) &&
|
|
||||||
(file?.equals(other.file, isDeep: isDeep) ?? true);
|
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +106,7 @@ class AlbumFileItem extends AlbumItem with EquatableMixin {
|
||||||
|
|
||||||
class AlbumLabelItem extends AlbumItem with EquatableMixin {
|
class AlbumLabelItem extends AlbumItem with EquatableMixin {
|
||||||
AlbumLabelItem({
|
AlbumLabelItem({
|
||||||
@required this.text,
|
required this.text,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AlbumLabelItem.fromJson(Map<String, dynamic> json) {
|
factory AlbumLabelItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
|
@ -50,7 +50,7 @@ abstract class AlbumProvider with EquatableMixin {
|
||||||
toString({bool isDeep = false});
|
toString({bool isDeep = false});
|
||||||
|
|
||||||
/// Return the date time associated with the latest item, or null
|
/// Return the date time associated with the latest item, or null
|
||||||
DateTime get latestItemTime;
|
DateTime? get latestItemTime;
|
||||||
|
|
||||||
AlbumProvider copyWith();
|
AlbumProvider copyWith();
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ abstract class AlbumProvider with EquatableMixin {
|
||||||
|
|
||||||
class AlbumStaticProvider extends AlbumProvider {
|
class AlbumStaticProvider extends AlbumProvider {
|
||||||
AlbumStaticProvider({
|
AlbumStaticProvider({
|
||||||
@required List<AlbumItem> items,
|
required List<AlbumItem> items,
|
||||||
}) : this.items = UnmodifiableListView(items);
|
}) : this.items = UnmodifiableListView(items);
|
||||||
|
|
||||||
factory AlbumStaticProvider.fromJson(Map<String, dynamic> json) {
|
factory AlbumStaticProvider.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -90,7 +90,9 @@ class AlbumStaticProvider extends AlbumProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AlbumStaticProvider copyWith({List<AlbumItem> items}) {
|
AlbumStaticProvider copyWith({
|
||||||
|
List<AlbumItem>? items,
|
||||||
|
}) {
|
||||||
return AlbumStaticProvider(
|
return AlbumStaticProvider(
|
||||||
items: items ?? this.items,
|
items: items ?? this.items,
|
||||||
);
|
);
|
||||||
|
@ -124,7 +126,7 @@ class AlbumStaticProvider extends AlbumProvider {
|
||||||
|
|
||||||
abstract class AlbumDynamicProvider extends AlbumProvider {
|
abstract class AlbumDynamicProvider extends AlbumProvider {
|
||||||
AlbumDynamicProvider({
|
AlbumDynamicProvider({
|
||||||
DateTime latestItemTime,
|
DateTime? latestItemTime,
|
||||||
}) : _latestItemTime = latestItemTime;
|
}) : _latestItemTime = latestItemTime;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -137,13 +139,13 @@ abstract class AlbumDynamicProvider extends AlbumProvider {
|
||||||
@override
|
@override
|
||||||
toContentJson() {
|
toContentJson() {
|
||||||
return {
|
return {
|
||||||
"latestItemTime": _latestItemTime?.toUtc()?.toIso8601String(),
|
"latestItemTime": _latestItemTime?.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AlbumDynamicProvider copyWith({
|
AlbumDynamicProvider copyWith({
|
||||||
DateTime latestItemTime,
|
DateTime? latestItemTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -154,13 +156,13 @@ abstract class AlbumDynamicProvider extends AlbumProvider {
|
||||||
_latestItemTime,
|
_latestItemTime,
|
||||||
];
|
];
|
||||||
|
|
||||||
final DateTime _latestItemTime;
|
final DateTime? _latestItemTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumDirProvider extends AlbumDynamicProvider {
|
class AlbumDirProvider extends AlbumDynamicProvider {
|
||||||
AlbumDirProvider({
|
AlbumDirProvider({
|
||||||
@required this.dirs,
|
required this.dirs,
|
||||||
DateTime latestItemTime,
|
DateTime? latestItemTime,
|
||||||
}) : super(latestItemTime: latestItemTime);
|
}) : super(latestItemTime: latestItemTime);
|
||||||
|
|
||||||
factory AlbumDirProvider.fromJson(Map<String, dynamic> json) {
|
factory AlbumDirProvider.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -192,8 +194,8 @@ class AlbumDirProvider extends AlbumDynamicProvider {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AlbumDirProvider copyWith({
|
AlbumDirProvider copyWith({
|
||||||
List<File> dirs,
|
List<File>? dirs,
|
||||||
DateTime latestItemTime,
|
DateTime? latestItemTime,
|
||||||
}) {
|
}) {
|
||||||
return AlbumDirProvider(
|
return AlbumDirProvider(
|
||||||
dirs: dirs ?? this.dirs,
|
dirs: dirs ?? this.dirs,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/entity/album/item.dart';
|
import 'package:nc_photos/entity/album/item.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
@ -80,7 +79,7 @@ class AlbumNullSortProvider extends AlbumSortProvider {
|
||||||
|
|
||||||
abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
|
abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
|
||||||
const AlbumReversibleSortProvider({
|
const AlbumReversibleSortProvider({
|
||||||
@required this.isAscending,
|
required this.isAscending,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -108,7 +107,7 @@ abstract class AlbumReversibleSortProvider extends AlbumSortProvider {
|
||||||
/// Sort based on the time of the files
|
/// Sort based on the time of the files
|
||||||
class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
||||||
const AlbumTimeSortProvider({
|
const AlbumTimeSortProvider({
|
||||||
bool isAscending,
|
required bool isAscending,
|
||||||
}) : super(isAscending: isAscending);
|
}) : super(isAscending: isAscending);
|
||||||
|
|
||||||
factory AlbumTimeSortProvider.fromJson(Map<String, dynamic> json) {
|
factory AlbumTimeSortProvider.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -126,7 +125,7 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
sort(List<AlbumItem> items) {
|
sort(List<AlbumItem> items) {
|
||||||
DateTime prevFileTime;
|
DateTime? prevFileTime;
|
||||||
return items
|
return items
|
||||||
.map((e) {
|
.map((e) {
|
||||||
if (e is AlbumFileItem) {
|
if (e is AlbumFileItem) {
|
||||||
|
@ -139,16 +138,16 @@ class AlbumTimeSortProvider extends AlbumReversibleSortProvider {
|
||||||
.stableSorted((x, y) {
|
.stableSorted((x, y) {
|
||||||
if (x.item1 == null && y.item1 == null) {
|
if (x.item1 == null && y.item1 == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
} else if (x.item1 == null) {
|
||||||
else if (x.item1 == null) {
|
|
||||||
return -1;
|
return -1;
|
||||||
} else if (y.item1 == null) {
|
} else if (y.item1 == null) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
|
||||||
if (isAscending) {
|
|
||||||
return x.item1.compareTo(y.item1);
|
|
||||||
} else {
|
} else {
|
||||||
return y.item1.compareTo(x.item1);
|
if (isAscending) {
|
||||||
|
return x.item1!.compareTo(y.item1!);
|
||||||
|
} else {
|
||||||
|
return y.item1!.compareTo(x.item1!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map((e) => e.item2)
|
.map((e) => e.item2)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
abstract class AlbumUpgrader {
|
abstract class AlbumUpgrader {
|
||||||
Map<String, dynamic> call(Map<String, dynamic> json);
|
Map<String, dynamic>? call(Map<String, dynamic> json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upgrade v1 Album to v2
|
/// Upgrade v1 Album to v2
|
||||||
|
@ -10,7 +10,8 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
|
||||||
this.logFilePath,
|
this.logFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
@override
|
||||||
|
call(Map<String, dynamic> json) {
|
||||||
// v1 album items are corrupted in one of the updates, drop it
|
// v1 album items are corrupted in one of the updates, drop it
|
||||||
_log.fine("[call] Upgrade v1 Album for file: $logFilePath");
|
_log.fine("[call] Upgrade v1 Album for file: $logFilePath");
|
||||||
final result = Map<String, dynamic>.from(json);
|
final result = Map<String, dynamic>.from(json);
|
||||||
|
@ -19,7 +20,7 @@ class AlbumUpgraderV1 implements AlbumUpgrader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String logFilePath;
|
final String? logFilePath;
|
||||||
|
|
||||||
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV1");
|
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV1");
|
||||||
}
|
}
|
||||||
|
@ -30,7 +31,8 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
||||||
this.logFilePath,
|
this.logFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
@override
|
||||||
|
call(Map<String, dynamic> json) {
|
||||||
// move v2 items to v3 provider
|
// move v2 items to v3 provider
|
||||||
_log.fine("[call] Upgrade v2 Album for file: $logFilePath");
|
_log.fine("[call] Upgrade v2 Album for file: $logFilePath");
|
||||||
final result = Map<String, dynamic>.from(json);
|
final result = Map<String, dynamic>.from(json);
|
||||||
|
@ -51,7 +53,7 @@ class AlbumUpgraderV2 implements AlbumUpgrader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String logFilePath;
|
final String? logFilePath;
|
||||||
|
|
||||||
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV2");
|
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV2");
|
||||||
}
|
}
|
||||||
|
@ -62,7 +64,8 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
||||||
this.logFilePath,
|
this.logFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
@override
|
||||||
|
call(Map<String, dynamic> json) {
|
||||||
// move v3 items to v4 provider
|
// move v3 items to v4 provider
|
||||||
_log.fine("[call] Upgrade v3 Album for file: $logFilePath");
|
_log.fine("[call] Upgrade v3 Album for file: $logFilePath");
|
||||||
final result = Map<String, dynamic>.from(json);
|
final result = Map<String, dynamic>.from(json);
|
||||||
|
@ -77,7 +80,7 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String logFilePath;
|
final String? logFilePath;
|
||||||
|
|
||||||
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV3");
|
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV3");
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,14 @@ class Exif with EquatableMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: hash_and_equals
|
// ignore: hash_and_equals
|
||||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||||
|
|
||||||
/// Compare two Exif objects
|
/// Compare two Exif objects
|
||||||
///
|
///
|
||||||
/// If [isDeep] is false, two Exif objects are considered identical if they
|
/// If [isDeep] is false, two Exif objects are considered identical if they
|
||||||
/// contain the same number of fields. This hack is to save time comparing a
|
/// contain the same number of fields. This hack is to save time comparing a
|
||||||
/// large amount of data that are mostly immutable
|
/// large amount of data that are mostly immutable
|
||||||
bool equals(Object other, {bool isDeep = false}) {
|
bool equals(Object? other, {bool isDeep = false}) {
|
||||||
if (isDeep) {
|
if (isDeep) {
|
||||||
return super == other;
|
return super == other;
|
||||||
} else {
|
} else {
|
||||||
|
@ -87,33 +87,33 @@ class Exif with EquatableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 0x010f Make
|
/// 0x010f Make
|
||||||
String get make => data["Make"];
|
String? get make => data["Make"];
|
||||||
|
|
||||||
/// 0x0110 Model
|
/// 0x0110 Model
|
||||||
String get model => data["Model"];
|
String? get model => data["Model"];
|
||||||
|
|
||||||
/// 0x9003 DateTimeOriginal
|
/// 0x9003 DateTimeOriginal
|
||||||
DateTime get dateTimeOriginal => data.containsKey("DateTimeOriginal")
|
DateTime? get dateTimeOriginal => data.containsKey("DateTimeOriginal")
|
||||||
? dateTimeFormat.parse(data["DateTimeOriginal"]).toUtc()
|
? dateTimeFormat.parse(data["DateTimeOriginal"]).toUtc()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
/// 0x829a ExposureTime
|
/// 0x829a ExposureTime
|
||||||
Rational get exposureTime => data["ExposureTime"];
|
Rational? get exposureTime => data["ExposureTime"];
|
||||||
|
|
||||||
/// 0x829d FNumber
|
/// 0x829d FNumber
|
||||||
Rational get fNumber => data["FNumber"];
|
Rational? get fNumber => data["FNumber"];
|
||||||
|
|
||||||
/// 0x8827 ISO/ISOSpeedRatings/PhotographicSensitivity
|
/// 0x8827 ISO/ISOSpeedRatings/PhotographicSensitivity
|
||||||
int get isoSpeedRatings => data["ISOSpeedRatings"];
|
int? get isoSpeedRatings => data["ISOSpeedRatings"];
|
||||||
|
|
||||||
/// 0x920a FocalLength
|
/// 0x920a FocalLength
|
||||||
Rational get focalLength => data["FocalLength"];
|
Rational? get focalLength => data["FocalLength"];
|
||||||
|
|
||||||
/// 0x8825 GPS tags
|
/// 0x8825 GPS tags
|
||||||
String get gpsLatitudeRef => data["GPSLatitudeRef"];
|
String? get gpsLatitudeRef => data["GPSLatitudeRef"];
|
||||||
List<Rational> get gpsLatitude => data["GPSLatitude"].cast<Rational>();
|
List<Rational>? get gpsLatitude => data["GPSLatitude"].cast<Rational>();
|
||||||
String get gpsLongitudeRef => data["GPSLongitudeRef"];
|
String? get gpsLongitudeRef => data["GPSLongitudeRef"];
|
||||||
List<Rational> get gpsLongitude => data["GPSLongitude"].cast<Rational>();
|
List<Rational>? get gpsLongitude => data["GPSLongitude"].cast<Rational>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get props => [
|
get props => [
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/exif.dart';
|
import 'package:nc_photos/entity/exif.dart';
|
||||||
|
@ -21,7 +20,7 @@ int compareFileDateTimeDescending(File x, File y) {
|
||||||
/// Immutable object that hold metadata of a [File]
|
/// Immutable object that hold metadata of a [File]
|
||||||
class Metadata with EquatableMixin {
|
class Metadata with EquatableMixin {
|
||||||
Metadata({
|
Metadata({
|
||||||
DateTime lastUpdated,
|
DateTime? lastUpdated,
|
||||||
this.fileEtag,
|
this.fileEtag,
|
||||||
this.imageWidth,
|
this.imageWidth,
|
||||||
this.imageHeight,
|
this.imageHeight,
|
||||||
|
@ -30,9 +29,9 @@ class Metadata with EquatableMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: hash_and_equals
|
// ignore: hash_and_equals
|
||||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||||
|
|
||||||
bool equals(Object other, {bool isDeep = false}) {
|
bool equals(Object? other, {bool isDeep = false}) {
|
||||||
if (other is Metadata) {
|
if (other is Metadata) {
|
||||||
return super == other &&
|
return super == other &&
|
||||||
(exif == null) == (other.exif == null) &&
|
(exif == null) == (other.exif == null) &&
|
||||||
|
@ -48,36 +47,37 @@ class Metadata with EquatableMixin {
|
||||||
/// corresponding upgrader will be called one by one to upgrade the json,
|
/// corresponding upgrader will be called one by one to upgrade the json,
|
||||||
/// version by version until it reached the active version. If any upgrader
|
/// version by version until it reached the active version. If any upgrader
|
||||||
/// in the chain is null, the upgrade process will fail
|
/// in the chain is null, the upgrade process will fail
|
||||||
factory Metadata.fromJson(
|
static Metadata? fromJson(
|
||||||
Map<String, dynamic> json, {
|
Map<String, dynamic> json, {
|
||||||
MetadataUpgraderV1 upgraderV1,
|
required MetadataUpgraderV1? upgraderV1,
|
||||||
MetadataUpgraderV2 upgraderV2,
|
required MetadataUpgraderV2? upgraderV2,
|
||||||
}) {
|
}) {
|
||||||
final jsonVersion = json["version"];
|
final jsonVersion = json["version"];
|
||||||
|
Map<String, dynamic>? result = json;
|
||||||
if (jsonVersion < 2) {
|
if (jsonVersion < 2) {
|
||||||
json = upgraderV1?.call(json);
|
result = upgraderV1?.call(result);
|
||||||
if (json == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (jsonVersion < 3) {
|
if (jsonVersion < 3) {
|
||||||
json = upgraderV2?.call(json);
|
result = upgraderV2?.call(result);
|
||||||
if (json == null) {
|
if (result == null) {
|
||||||
_log.info("[fromJson] Version $jsonVersion not compatible");
|
_log.info("[fromJson] Version $jsonVersion not compatible");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Metadata(
|
return Metadata(
|
||||||
lastUpdated: json["lastUpdated"] == null
|
lastUpdated: result["lastUpdated"] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json["lastUpdated"]),
|
: DateTime.parse(result["lastUpdated"]),
|
||||||
fileEtag: json["fileEtag"],
|
fileEtag: result["fileEtag"],
|
||||||
imageWidth: json["imageWidth"],
|
imageWidth: result["imageWidth"],
|
||||||
imageHeight: json["imageHeight"],
|
imageHeight: result["imageHeight"],
|
||||||
exif: json["exif"] == null
|
exif: result["exif"] == null
|
||||||
? null
|
? null
|
||||||
: Exif.fromJson(json["exif"].cast<String, dynamic>()),
|
: Exif.fromJson(result["exif"].cast<String, dynamic>()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ class Metadata with EquatableMixin {
|
||||||
if (fileEtag != null) "fileEtag": fileEtag,
|
if (fileEtag != null) "fileEtag": fileEtag,
|
||||||
if (imageWidth != null) "imageWidth": imageWidth,
|
if (imageWidth != null) "imageWidth": imageWidth,
|
||||||
if (imageHeight != null) "imageHeight": imageHeight,
|
if (imageHeight != null) "imageHeight": imageHeight,
|
||||||
if (exif != null) "exif": exif.toJson(),
|
if (exif != null) "exif": exif!.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,10 +123,10 @@ class Metadata with EquatableMixin {
|
||||||
final DateTime lastUpdated;
|
final DateTime lastUpdated;
|
||||||
|
|
||||||
/// Etag of the parent file when the metadata is saved
|
/// Etag of the parent file when the metadata is saved
|
||||||
final String fileEtag;
|
final String? fileEtag;
|
||||||
final int imageWidth;
|
final int? imageWidth;
|
||||||
final int imageHeight;
|
final int? imageHeight;
|
||||||
final Exif exif;
|
final Exif? exif;
|
||||||
|
|
||||||
/// versioning of this class, use to upgrade old persisted metadata
|
/// versioning of this class, use to upgrade old persisted metadata
|
||||||
static const version = 3;
|
static const version = 3;
|
||||||
|
@ -135,17 +135,17 @@ class Metadata with EquatableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class MetadataUpgrader {
|
abstract class MetadataUpgrader {
|
||||||
Map<String, dynamic> call(Map<String, dynamic> json);
|
Map<String, dynamic>? call(Map<String, dynamic> json);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upgrade v1 Metadata to v2
|
/// Upgrade v1 Metadata to v2
|
||||||
class MetadataUpgraderV1 implements MetadataUpgrader {
|
class MetadataUpgraderV1 implements MetadataUpgrader {
|
||||||
MetadataUpgraderV1({
|
MetadataUpgraderV1({
|
||||||
@required this.fileContentType,
|
required this.fileContentType,
|
||||||
this.logFilePath,
|
this.logFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
Map<String, dynamic>? call(Map<String, dynamic> json) {
|
||||||
if (fileContentType == "image/webp") {
|
if (fileContentType == "image/webp") {
|
||||||
// Version 1 metadata for webp is bugged, drop it
|
// Version 1 metadata for webp is bugged, drop it
|
||||||
_log.fine("[call] Upgrade v1 metadata for file: $logFilePath");
|
_log.fine("[call] Upgrade v1 metadata for file: $logFilePath");
|
||||||
|
@ -155,10 +155,10 @@ class MetadataUpgraderV1 implements MetadataUpgrader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String fileContentType;
|
final String? fileContentType;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String logFilePath;
|
final String? logFilePath;
|
||||||
|
|
||||||
static final _log = Logger("entity.file.MetadataUpgraderV1");
|
static final _log = Logger("entity.file.MetadataUpgraderV1");
|
||||||
}
|
}
|
||||||
|
@ -166,11 +166,11 @@ class MetadataUpgraderV1 implements MetadataUpgrader {
|
||||||
/// Upgrade v2 Metadata to v3
|
/// Upgrade v2 Metadata to v3
|
||||||
class MetadataUpgraderV2 implements MetadataUpgrader {
|
class MetadataUpgraderV2 implements MetadataUpgrader {
|
||||||
MetadataUpgraderV2({
|
MetadataUpgraderV2({
|
||||||
@required this.fileContentType,
|
required this.fileContentType,
|
||||||
this.logFilePath,
|
this.logFilePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> call(Map<String, dynamic> json) {
|
Map<String, dynamic>? call(Map<String, dynamic> json) {
|
||||||
if (fileContentType == "image/jpeg") {
|
if (fileContentType == "image/jpeg") {
|
||||||
// Version 2 metadata for jpeg doesn't consider orientation
|
// Version 2 metadata for jpeg doesn't consider orientation
|
||||||
if (json["exif"] != null && json["exif"].containsKey("Orientation")) {
|
if (json["exif"] != null && json["exif"].containsKey("Orientation")) {
|
||||||
|
@ -187,17 +187,17 @@ class MetadataUpgraderV2 implements MetadataUpgrader {
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String fileContentType;
|
final String? fileContentType;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String logFilePath;
|
final String? logFilePath;
|
||||||
|
|
||||||
static final _log = Logger("entity.file.MetadataUpgraderV2");
|
static final _log = Logger("entity.file.MetadataUpgraderV2");
|
||||||
}
|
}
|
||||||
|
|
||||||
class File with EquatableMixin {
|
class File with EquatableMixin {
|
||||||
File({
|
File({
|
||||||
@required String path,
|
required String path,
|
||||||
this.contentLength,
|
this.contentLength,
|
||||||
this.contentType,
|
this.contentType,
|
||||||
this.etag,
|
this.etag,
|
||||||
|
@ -214,9 +214,9 @@ class File with EquatableMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: hash_and_equals
|
// ignore: hash_and_equals
|
||||||
bool operator ==(Object other) => equals(other, isDeep: true);
|
bool operator ==(Object? other) => equals(other, isDeep: true);
|
||||||
|
|
||||||
bool equals(Object other, {bool isDeep = false}) {
|
bool equals(Object? other, {bool isDeep = false}) {
|
||||||
if (other is File) {
|
if (other is File) {
|
||||||
return super == other &&
|
return super == other &&
|
||||||
(metadata == null) == (other.metadata == null) &&
|
(metadata == null) == (other.metadata == null) &&
|
||||||
|
@ -310,33 +310,33 @@ class File with EquatableMixin {
|
||||||
if (contentType != null) "contentType": contentType,
|
if (contentType != null) "contentType": contentType,
|
||||||
if (etag != null) "etag": etag,
|
if (etag != null) "etag": etag,
|
||||||
if (lastModified != null)
|
if (lastModified != null)
|
||||||
"lastModified": lastModified.toUtc().toIso8601String(),
|
"lastModified": lastModified!.toUtc().toIso8601String(),
|
||||||
if (isCollection != null) "isCollection": isCollection,
|
if (isCollection != null) "isCollection": isCollection,
|
||||||
if (usedBytes != null) "usedBytes": usedBytes,
|
if (usedBytes != null) "usedBytes": usedBytes,
|
||||||
if (hasPreview != null) "hasPreview": hasPreview,
|
if (hasPreview != null) "hasPreview": hasPreview,
|
||||||
if (fileId != null) "fileId": fileId,
|
if (fileId != null) "fileId": fileId,
|
||||||
if (ownerId != null) "ownerId": ownerId,
|
if (ownerId != null) "ownerId": ownerId,
|
||||||
if (metadata != null) "metadata": metadata.toJson(),
|
if (metadata != null) "metadata": metadata!.toJson(),
|
||||||
if (isArchived != null) "isArchived": isArchived,
|
if (isArchived != null) "isArchived": isArchived,
|
||||||
if (overrideDateTime != null)
|
if (overrideDateTime != null)
|
||||||
"overrideDateTime": overrideDateTime.toUtc().toIso8601String(),
|
"overrideDateTime": overrideDateTime!.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
File copyWith({
|
File copyWith({
|
||||||
String path,
|
String? path,
|
||||||
int contentLength,
|
int? contentLength,
|
||||||
String contentType,
|
String? contentType,
|
||||||
String etag,
|
String? etag,
|
||||||
DateTime lastModified,
|
DateTime? lastModified,
|
||||||
bool isCollection,
|
bool? isCollection,
|
||||||
int usedBytes,
|
int? usedBytes,
|
||||||
bool hasPreview,
|
bool? hasPreview,
|
||||||
int fileId,
|
int? fileId,
|
||||||
String ownerId,
|
String? ownerId,
|
||||||
OrNull<Metadata> metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool> isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime> overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
}) {
|
}) {
|
||||||
return File(
|
return File(
|
||||||
path: path ?? this.path,
|
path: path ?? this.path,
|
||||||
|
@ -392,20 +392,20 @@ class File with EquatableMixin {
|
||||||
];
|
];
|
||||||
|
|
||||||
final String path;
|
final String path;
|
||||||
final int contentLength;
|
final int? contentLength;
|
||||||
final String contentType;
|
final String? contentType;
|
||||||
final String etag;
|
final String? etag;
|
||||||
final DateTime lastModified;
|
final DateTime? lastModified;
|
||||||
final bool isCollection;
|
final bool? isCollection;
|
||||||
final int usedBytes;
|
final int? usedBytes;
|
||||||
final bool hasPreview;
|
final bool? hasPreview;
|
||||||
// maybe null when loaded from old cache
|
// maybe null when loaded from old cache
|
||||||
final int fileId;
|
final int? fileId;
|
||||||
final String ownerId;
|
final String? ownerId;
|
||||||
// metadata
|
// metadata
|
||||||
final Metadata metadata;
|
final Metadata? metadata;
|
||||||
final bool isArchived;
|
final bool? isArchived;
|
||||||
final DateTime overrideDateTime;
|
final DateTime? overrideDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension FileExtension on File {
|
extension FileExtension on File {
|
||||||
|
@ -442,9 +442,9 @@ class FileRepo {
|
||||||
Future<void> updateProperty(
|
Future<void> updateProperty(
|
||||||
Account account,
|
Account account,
|
||||||
File file, {
|
File file, {
|
||||||
OrNull<Metadata> metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool> isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime> overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
}) =>
|
}) =>
|
||||||
this.dataSrc.updateProperty(
|
this.dataSrc.updateProperty(
|
||||||
account,
|
account,
|
||||||
|
@ -459,7 +459,7 @@ class FileRepo {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) =>
|
}) =>
|
||||||
this.dataSrc.copy(
|
this.dataSrc.copy(
|
||||||
account,
|
account,
|
||||||
|
@ -473,7 +473,7 @@ class FileRepo {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) =>
|
}) =>
|
||||||
this.dataSrc.move(
|
this.dataSrc.move(
|
||||||
account,
|
account,
|
||||||
|
@ -506,9 +506,9 @@ abstract class FileDataSource {
|
||||||
Future<void> updateProperty(
|
Future<void> updateProperty(
|
||||||
Account account,
|
Account account,
|
||||||
File f, {
|
File f, {
|
||||||
OrNull<Metadata> metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool> isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime> overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Copy [f] to [destination]
|
/// Copy [f] to [destination]
|
||||||
|
@ -519,7 +519,7 @@ abstract class FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Move [f] to [destination]
|
/// Move [f] to [destination]
|
||||||
|
@ -530,7 +530,7 @@ abstract class FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Create a directory at [path]
|
/// Create a directory at [path]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:idb_shim/idb_client.dart';
|
import 'package:idb_shim/idb_client.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
|
@ -27,7 +26,7 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
list(
|
list(
|
||||||
Account account,
|
Account account,
|
||||||
File f, {
|
File f, {
|
||||||
int depth,
|
int? depth,
|
||||||
}) async {
|
}) async {
|
||||||
_log.fine("[list] ${f.path}");
|
_log.fine("[list] ${f.path}");
|
||||||
final response = await Api(account).files().propfind(
|
final response = await Api(account).files().propfind(
|
||||||
|
@ -61,7 +60,7 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
final files = WebdavFileParser()(xml);
|
final files = WebdavFileParser()(xml);
|
||||||
// _log.fine("[list] Parsed files: [$files]");
|
// _log.fine("[list] Parsed files: [$files]");
|
||||||
return files.where((element) => _validateFile(element)).map((e) {
|
return files.where((element) => _validateFile(element)).map((e) {
|
||||||
if (e.metadata == null || e.metadata.fileEtag == e.etag) {
|
if (e.metadata == null || e.metadata!.fileEtag == e.etag) {
|
||||||
return e;
|
return e;
|
||||||
} else {
|
} else {
|
||||||
_log.info("[list] Ignore outdated metadata for ${e.path}");
|
_log.info("[list] Ignore outdated metadata for ${e.path}");
|
||||||
|
@ -112,27 +111,27 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
updateProperty(
|
updateProperty(
|
||||||
Account account,
|
Account account,
|
||||||
File f, {
|
File f, {
|
||||||
OrNull<Metadata> metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool> isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime> overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
}) async {
|
}) async {
|
||||||
_log.info("[updateProperty] ${f.path}");
|
_log.info("[updateProperty] ${f.path}");
|
||||||
if (metadata?.obj != null && metadata.obj.fileEtag != f.etag) {
|
if (metadata?.obj != null && metadata!.obj!.fileEtag != f.etag) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj.fileEtag}, file: ${f.etag})");
|
"[updateProperty] Metadata etag mismatch (metadata: ${metadata.obj!.fileEtag}, file: ${f.etag})");
|
||||||
}
|
}
|
||||||
final setProps = {
|
final setProps = {
|
||||||
if (metadata?.obj != null)
|
if (metadata?.obj != null)
|
||||||
"app:metadata": jsonEncode(metadata.obj.toJson()),
|
"app:metadata": jsonEncode(metadata!.obj!.toJson()),
|
||||||
if (isArchived?.obj != null) "app:is-archived": isArchived.obj,
|
if (isArchived?.obj != null) "app:is-archived": isArchived!.obj,
|
||||||
if (overrideDateTime?.obj != null)
|
if (overrideDateTime?.obj != null)
|
||||||
"app:override-date-time":
|
"app:override-date-time":
|
||||||
overrideDateTime.obj.toUtc().toIso8601String(),
|
overrideDateTime!.obj!.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
final removeProps = [
|
final removeProps = [
|
||||||
if (OrNull.isNull(metadata)) "app:metadata",
|
if (OrNull.isSetNull(metadata)) "app:metadata",
|
||||||
if (OrNull.isNull(isArchived)) "app:is-archived",
|
if (OrNull.isSetNull(isArchived)) "app:is-archived",
|
||||||
if (OrNull.isNull(overrideDateTime)) "app:override-date-time",
|
if (OrNull.isSetNull(overrideDateTime)) "app:override-date-time",
|
||||||
];
|
];
|
||||||
final response = await Api(account).files().proppatch(
|
final response = await Api(account).files().proppatch(
|
||||||
path: f.path,
|
path: f.path,
|
||||||
|
@ -155,7 +154,7 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) async {
|
}) async {
|
||||||
_log.info("[copy] ${f.path} to $destination");
|
_log.info("[copy] ${f.path} to $destination");
|
||||||
final response = await Api(account).files().copy(
|
final response = await Api(account).files().copy(
|
||||||
|
@ -176,7 +175,7 @@ class FileWebdavDataSource implements FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) async {
|
}) async {
|
||||||
_log.info("[move] ${f.path} to $destination");
|
_log.info("[move] ${f.path} to $destination");
|
||||||
final response = await Api(account).files().move(
|
final response = await Api(account).files().move(
|
||||||
|
@ -256,9 +255,9 @@ class FileAppDbDataSource implements FileDataSource {
|
||||||
updateProperty(
|
updateProperty(
|
||||||
Account account,
|
Account account,
|
||||||
File f, {
|
File f, {
|
||||||
OrNull<Metadata> metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool> isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime> overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
}) {
|
}) {
|
||||||
_log.info("[updateProperty] ${f.path}");
|
_log.info("[updateProperty] ${f.path}");
|
||||||
return AppDb.use((db) async {
|
return AppDb.use((db) async {
|
||||||
|
@ -300,7 +299,7 @@ class FileAppDbDataSource implements FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) async {
|
}) async {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
@ -310,7 +309,7 @@ class FileAppDbDataSource implements FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) async {
|
}) async {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
@ -325,7 +324,7 @@ class FileAppDbDataSource implements FileDataSource {
|
||||||
final path = AppDbFileEntry.toPath(account, f);
|
final path = AppDbFileEntry.toPath(account, f);
|
||||||
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
final range = KeyRange.bound([path, 0], [path, int_util.int32Max]);
|
||||||
final List results = await index.getAll(range);
|
final List results = await index.getAll(range);
|
||||||
if (results?.isNotEmpty == true) {
|
if (results.isNotEmpty == true) {
|
||||||
final entries = results
|
final entries = results
|
||||||
.map((e) => AppDbFileEntry.fromJson(e.cast<String, dynamic>()));
|
.map((e) => AppDbFileEntry.fromJson(e.cast<String, dynamic>()));
|
||||||
return entries
|
return entries
|
||||||
|
@ -358,7 +357,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
);
|
);
|
||||||
final cache = await cacheManager.list(account, f);
|
final cache = await cacheManager.list(account, f);
|
||||||
if (cacheManager.isGood) {
|
if (cacheManager.isGood) {
|
||||||
return cache;
|
return cache!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no cache or outdated
|
// no cache or outdated
|
||||||
|
@ -426,9 +425,9 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
updateProperty(
|
updateProperty(
|
||||||
Account account,
|
Account account,
|
||||||
File f, {
|
File f, {
|
||||||
OrNull<Metadata> metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool> isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime> overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
}) async {
|
}) async {
|
||||||
await _remoteSrc
|
await _remoteSrc
|
||||||
.updateProperty(
|
.updateProperty(
|
||||||
|
@ -462,7 +461,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) async {
|
}) async {
|
||||||
await _remoteSrc.copy(account, f, destination,
|
await _remoteSrc.copy(account, f, destination,
|
||||||
shouldOverwrite: shouldOverwrite);
|
shouldOverwrite: shouldOverwrite);
|
||||||
|
@ -473,7 +472,7 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
Account account,
|
Account account,
|
||||||
File f,
|
File f,
|
||||||
String destination, {
|
String destination, {
|
||||||
bool shouldOverwrite,
|
bool? shouldOverwrite,
|
||||||
}) async {
|
}) async {
|
||||||
await _remoteSrc.move(account, f, destination,
|
await _remoteSrc.move(account, f, destination,
|
||||||
shouldOverwrite: shouldOverwrite);
|
shouldOverwrite: shouldOverwrite);
|
||||||
|
@ -599,17 +598,17 @@ class FileCachedDataSource implements FileDataSource {
|
||||||
|
|
||||||
class _CacheManager {
|
class _CacheManager {
|
||||||
_CacheManager({
|
_CacheManager({
|
||||||
@required this.appDbSrc,
|
required this.appDbSrc,
|
||||||
@required this.remoteSrc,
|
required this.remoteSrc,
|
||||||
this.shouldCheckCache = false,
|
this.shouldCheckCache = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Return the cached results of listing a directory [f]
|
/// Return the cached results of listing a directory [f]
|
||||||
///
|
///
|
||||||
/// Should check [isGood] before using the cache returning by this method
|
/// Should check [isGood] before using the cache returning by this method
|
||||||
Future<List<File>> list(Account account, File f) async {
|
Future<List<File>?> list(Account account, File f) async {
|
||||||
final trimmedRootPath = f.path.trimAny("/");
|
final trimmedRootPath = f.path.trimAny("/");
|
||||||
List<File> cache;
|
List<File>? cache;
|
||||||
try {
|
try {
|
||||||
cache = await appDbSrc.list(account, f);
|
cache = await appDbSrc.list(account, f);
|
||||||
// compare the cached root
|
// compare the cached root
|
||||||
|
@ -645,7 +644,7 @@ class _CacheManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isGood => _isGood;
|
bool get isGood => _isGood;
|
||||||
String get remoteTouchToken => _remoteToken;
|
String? get remoteTouchToken => _remoteToken;
|
||||||
|
|
||||||
Future<void> _checkTouchToken(
|
Future<void> _checkTouchToken(
|
||||||
Account account, File f, List<File> cache) async {
|
Account account, File f, List<File> cache) async {
|
||||||
|
@ -653,7 +652,7 @@ class _CacheManager {
|
||||||
"${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}";
|
"${remote_storage_util.getRemoteTouchDir(account)}/${f.strippedPath}";
|
||||||
final fileRepo = FileRepo(FileCachedDataSource());
|
final fileRepo = FileRepo(FileCachedDataSource());
|
||||||
final tokenManager = TouchTokenManager();
|
final tokenManager = TouchTokenManager();
|
||||||
String remoteToken;
|
String? remoteToken;
|
||||||
try {
|
try {
|
||||||
remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f);
|
remoteToken = await tokenManager.getRemoteToken(fileRepo, account, f);
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
|
@ -664,7 +663,7 @@ class _CacheManager {
|
||||||
}
|
}
|
||||||
_remoteToken = remoteToken;
|
_remoteToken = remoteToken;
|
||||||
|
|
||||||
String localToken;
|
String? localToken;
|
||||||
try {
|
try {
|
||||||
localToken = await tokenManager.getLocalToken(account, f);
|
localToken = await tokenManager.getLocalToken(account, f);
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
|
@ -687,7 +686,7 @@ class _CacheManager {
|
||||||
final bool shouldCheckCache;
|
final bool shouldCheckCache;
|
||||||
|
|
||||||
var _isGood = false;
|
var _isGood = false;
|
||||||
String _remoteToken;
|
String? _remoteToken;
|
||||||
|
|
||||||
static final _log = Logger("entity.file.data_source._CacheManager");
|
static final _log = Logger("entity.file.data_source._CacheManager");
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ class WebdavFileParser {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.where((element) => element != null)
|
.whereType<File>()
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,19 +55,19 @@ class WebdavFileParser {
|
||||||
|
|
||||||
/// Map <DAV:response> contents to File
|
/// Map <DAV:response> contents to File
|
||||||
File _toFile(XmlElement element) {
|
File _toFile(XmlElement element) {
|
||||||
String path;
|
String? path;
|
||||||
int contentLength;
|
int? contentLength;
|
||||||
String contentType;
|
String? contentType;
|
||||||
String etag;
|
String? etag;
|
||||||
DateTime lastModified;
|
DateTime? lastModified;
|
||||||
bool isCollection;
|
bool? isCollection;
|
||||||
int usedBytes;
|
int? usedBytes;
|
||||||
bool hasPreview;
|
bool? hasPreview;
|
||||||
int fileId;
|
int? fileId;
|
||||||
String ownerId;
|
String? ownerId;
|
||||||
Metadata metadata;
|
Metadata? metadata;
|
||||||
bool isArchived;
|
bool? isArchived;
|
||||||
DateTime overrideDateTime;
|
DateTime? overrideDateTime;
|
||||||
|
|
||||||
for (final child in element.children.whereType<XmlElement>()) {
|
for (final child in element.children.whereType<XmlElement>()) {
|
||||||
if (child.matchQualifiedName("href",
|
if (child.matchQualifiedName("href",
|
||||||
|
@ -105,7 +105,7 @@ class WebdavFileParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
return File(
|
return File(
|
||||||
path: path,
|
path: path!,
|
||||||
contentLength: contentLength,
|
contentLength: contentLength,
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
etag: etag,
|
etag: etag,
|
||||||
|
@ -140,7 +140,10 @@ class WebdavFileParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PropParser {
|
class _PropParser {
|
||||||
_PropParser({this.namespaces = const {}, this.logFilePath});
|
_PropParser({
|
||||||
|
this.namespaces = const {},
|
||||||
|
this.logFilePath,
|
||||||
|
});
|
||||||
|
|
||||||
/// Parse <DAV:prop> element contents
|
/// Parse <DAV:prop> element contents
|
||||||
void parse(XmlElement element) {
|
void parse(XmlElement element) {
|
||||||
|
@ -201,43 +204,43 @@ class _PropParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime get lastModified => _lastModified;
|
DateTime? get lastModified => _lastModified;
|
||||||
int get contentLength => _contentLength;
|
int? get contentLength => _contentLength;
|
||||||
String get contentType => _contentType;
|
String? get contentType => _contentType;
|
||||||
String get etag => _etag;
|
String? get etag => _etag;
|
||||||
int get usedBytes => _usedBytes;
|
int? get usedBytes => _usedBytes;
|
||||||
bool get isCollection => _isCollection;
|
bool? get isCollection => _isCollection;
|
||||||
bool get hasPreview => _hasPreview;
|
bool? get hasPreview => _hasPreview;
|
||||||
int get fileId => _fileId;
|
int? get fileId => _fileId;
|
||||||
String get ownerId => _ownerId;
|
String? get ownerId => _ownerId;
|
||||||
Metadata get metadata => _metadata;
|
Metadata? get metadata => _metadata;
|
||||||
bool get isArchived => _isArchived;
|
bool? get isArchived => _isArchived;
|
||||||
DateTime get overrideDateTime => _overrideDateTime;
|
DateTime? get overrideDateTime => _overrideDateTime;
|
||||||
|
|
||||||
final Map<String, String> namespaces;
|
final Map<String, String> namespaces;
|
||||||
|
|
||||||
/// File path for logging only
|
/// File path for logging only
|
||||||
final String logFilePath;
|
final String? logFilePath;
|
||||||
|
|
||||||
DateTime _lastModified;
|
DateTime? _lastModified;
|
||||||
int _contentLength;
|
int? _contentLength;
|
||||||
String _contentType;
|
String? _contentType;
|
||||||
String _etag;
|
String? _etag;
|
||||||
int _usedBytes;
|
int? _usedBytes;
|
||||||
bool _isCollection;
|
bool? _isCollection;
|
||||||
bool _hasPreview;
|
bool? _hasPreview;
|
||||||
int _fileId;
|
int? _fileId;
|
||||||
String _ownerId;
|
String? _ownerId;
|
||||||
Metadata _metadata;
|
Metadata? _metadata;
|
||||||
bool _isArchived;
|
bool? _isArchived;
|
||||||
DateTime _overrideDateTime;
|
DateTime? _overrideDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on XmlElement {
|
extension on XmlElement {
|
||||||
bool matchQualifiedName(
|
bool matchQualifiedName(
|
||||||
String local, {
|
String local, {
|
||||||
String prefix,
|
required String prefix,
|
||||||
Map<String, String> namespaces,
|
required Map<String, String> namespaces,
|
||||||
}) {
|
}) {
|
||||||
final localNamespaces = <String, String>{};
|
final localNamespaces = <String, String>{};
|
||||||
for (final a in attributes) {
|
for (final a in attributes) {
|
||||||
|
|
|
@ -23,13 +23,13 @@ class AppEventListener<T> {
|
||||||
_log.warning("[endListenEvent] Already not listening");
|
_log.warning("[endListenEvent] Already not listening");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_subscription.cancel();
|
_subscription?.cancel();
|
||||||
_subscription = null;
|
_subscription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final void Function(T) _listener;
|
final void Function(T) _listener;
|
||||||
final _stream = KiwiContainer().resolve<EventBus>().on<T>();
|
final _stream = KiwiContainer().resolve<EventBus>().on<T>();
|
||||||
StreamSubscription<T> _subscription;
|
StreamSubscription<T>? _subscription;
|
||||||
|
|
||||||
final _log = Logger("event.event.AppEventListener<${T.runtimeType}>");
|
final _log = Logger("event.event.AppEventListener<${T.runtimeType}>");
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,10 @@ class CacheNotFoundException implements Exception {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiException implements Exception {
|
class ApiException implements Exception {
|
||||||
ApiException({this.response, this.message});
|
ApiException({
|
||||||
|
required this.response,
|
||||||
|
this.message,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() {
|
toString() {
|
||||||
|
|
|
@ -10,16 +10,16 @@ import 'package:nc_photos/exception.dart';
|
||||||
String toUserString(dynamic exception, BuildContext context) {
|
String toUserString(dynamic exception, BuildContext context) {
|
||||||
if (exception is ApiException) {
|
if (exception is ApiException) {
|
||||||
if (exception.response.statusCode == 401) {
|
if (exception.response.statusCode == 401) {
|
||||||
return AppLocalizations.of(context).errorUnauthenticated;
|
return AppLocalizations.of(context)!.errorUnauthenticated;
|
||||||
} else if (exception.response.statusCode == 423) {
|
} else if (exception.response.statusCode == 423) {
|
||||||
return AppLocalizations.of(context).errorLocked;
|
return AppLocalizations.of(context)!.errorLocked;
|
||||||
} else if (exception.response.statusCode == 500) {
|
} else if (exception.response.statusCode == 500) {
|
||||||
return AppLocalizations.of(context).errorServerError;
|
return AppLocalizations.of(context)!.errorServerError;
|
||||||
}
|
}
|
||||||
} else if (exception is SocketException) {
|
} else if (exception is SocketException) {
|
||||||
return AppLocalizations.of(context).errorDisconnected;
|
return AppLocalizations.of(context)!.errorDisconnected;
|
||||||
} else if (exception is InvalidBaseUrlException) {
|
} else if (exception is InvalidBaseUrlException) {
|
||||||
return AppLocalizations.of(context).errorInvalidBaseUrl;
|
return AppLocalizations.of(context)!.errorInvalidBaseUrl;
|
||||||
}
|
}
|
||||||
return exception.toString();
|
return exception.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
extension IterableExtension<T> on Iterable<T> {
|
extension IterableExtension<T> on Iterable<T> {
|
||||||
/// Return a new sorted list
|
/// Return a new sorted list
|
||||||
List<T> sorted([int compare(T a, T b)]) => this.toList()..sort(compare);
|
List<T> sorted([int compare(T a, T b)?]) => this.toList()..sort(compare);
|
||||||
|
|
||||||
/// Return a new stable sorted list
|
/// Return a new stable sorted list
|
||||||
List<T> stableSorted([int compare(T a, T b)]) {
|
List<T> stableSorted([int compare(T a, T b)?]) {
|
||||||
final tmp = this.toList();
|
final tmp = this.toList();
|
||||||
mergeSort(tmp, compare: compare);
|
mergeSort(tmp, compare: compare);
|
||||||
return tmp;
|
return tmp;
|
||||||
|
|
|
@ -6,12 +6,12 @@ class AppLanguage {
|
||||||
|
|
||||||
final int langId;
|
final int langId;
|
||||||
final String nativeName;
|
final String nativeName;
|
||||||
final Locale locale;
|
final Locale? locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getSelectedLanguageName(BuildContext context) =>
|
String getSelectedLanguageName(BuildContext context) =>
|
||||||
_getSelectedLanguage(context).nativeName;
|
_getSelectedLanguage(context).nativeName;
|
||||||
Locale getSelectedLocale(BuildContext context) =>
|
Locale? getSelectedLocale(BuildContext context) =>
|
||||||
_getSelectedLanguage(context).locale;
|
_getSelectedLanguage(context).locale;
|
||||||
|
|
||||||
final supportedLanguages = {
|
final supportedLanguages = {
|
||||||
|
@ -32,9 +32,9 @@ enum _AppLanguageEnum {
|
||||||
|
|
||||||
AppLanguage _getSelectedLanguage(BuildContext context) {
|
AppLanguage _getSelectedLanguage(BuildContext context) {
|
||||||
try {
|
try {
|
||||||
final lang = Pref.inst().getLanguage();
|
final lang = Pref.inst().getLanguageOr(0);
|
||||||
return supportedLanguages[lang];
|
return supportedLanguages[lang]!;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return supportedLanguages[_AppLanguageEnum.systemDefault.index];
|
return supportedLanguages[_AppLanguageEnum.systemDefault.index]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,8 +72,8 @@ void _initLog() {
|
||||||
|
|
||||||
Future<void> _initPref() async {
|
Future<void> _initPref() async {
|
||||||
await Pref.init();
|
await Pref.init();
|
||||||
if (Pref.inst().getLastVersion(null) == null) {
|
if (Pref.inst().getLastVersion() == null) {
|
||||||
if (Pref.inst().getSetupProgress(null) == null) {
|
if (Pref.inst().getSetupProgress() == null) {
|
||||||
// new install
|
// new install
|
||||||
await Pref.inst().setLastVersion(k.version);
|
await Pref.inst().setLastVersion(k.version);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -25,7 +25,7 @@ class MetadataTask {
|
||||||
final op = UpdateMissingMetadata(fileRepo);
|
final op = UpdateMissingMetadata(fileRepo);
|
||||||
await for (final _ in op(account,
|
await for (final _ in op(account,
|
||||||
File(path: "${api_util.getWebdavRootUrlRelative(account)}/$r"))) {
|
File(path: "${api_util.getWebdavRootUrlRelative(account)}/$r"))) {
|
||||||
if (!Pref.inst().isEnableExif()) {
|
if (!Pref.inst().isEnableExifOr()) {
|
||||||
_log.info("[call] EXIF disabled, task ending immaturely");
|
_log.info("[call] EXIF disabled, task ending immaturely");
|
||||||
op.stop();
|
op.stop();
|
||||||
return;
|
return;
|
||||||
|
@ -53,7 +53,7 @@ class MetadataTaskManager {
|
||||||
|
|
||||||
void _handleStream() async {
|
void _handleStream() async {
|
||||||
await for (final task in _streamController.stream) {
|
await for (final task in _streamController.stream) {
|
||||||
if (Pref.inst().isEnableExif()) {
|
if (Pref.inst().isEnableExifOr()) {
|
||||||
_log.info("[_doTask] Executing task: $task");
|
_log.info("[_doTask] Executing task: $task");
|
||||||
await task();
|
await task();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,11 +6,13 @@ class MediaStore {
|
||||||
static const exceptionCodePermissionError = "permissionError";
|
static const exceptionCodePermissionError = "permissionError";
|
||||||
|
|
||||||
static Future<String> saveFileToDownload(
|
static Future<String> saveFileToDownload(
|
||||||
String fileName, Uint8List fileContent) =>
|
String fileName, Uint8List fileContent) async {
|
||||||
_channel.invokeMethod("saveFileToDownload", <String, dynamic>{
|
return (await _channel
|
||||||
"fileName": fileName,
|
.invokeMethod<String>("saveFileToDownload", <String, dynamic>{
|
||||||
"content": fileContent,
|
"fileName": fileName,
|
||||||
});
|
"content": fileContent,
|
||||||
|
}))!;
|
||||||
|
}
|
||||||
|
|
||||||
static const _channel =
|
static const _channel =
|
||||||
const MethodChannel("com.nkming.nc_photos/media_store");
|
const MethodChannel("com.nkming.nc_photos/media_store");
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class Notification {
|
class Notification {
|
||||||
static Future<void> notifyItemsDownloadSuccessful(
|
static Future<void> notifyItemsDownloadSuccessful(
|
||||||
List<String> fileUris, List<String> mimeTypes) =>
|
List<String> fileUris, List<String?> mimeTypes) =>
|
||||||
_channel.invokeMethod("notifyItemsDownloadSuccessful", <String, dynamic>{
|
_channel.invokeMethod("notifyItemsDownloadSuccessful", <String, dynamic>{
|
||||||
"fileUris": fileUris,
|
"fileUris": fileUris,
|
||||||
"mimeTypes": mimeTypes,
|
"mimeTypes": mimeTypes,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class Share {
|
class Share {
|
||||||
static Future<void> shareItems(
|
static Future<void> shareItems(
|
||||||
List<String> fileUris, List<String> mimeTypes) =>
|
List<String> fileUris, List<String?> mimeTypes) =>
|
||||||
_channel.invokeMethod("shareItems", <String, dynamic>{
|
_channel.invokeMethod("shareItems", <String, dynamic>{
|
||||||
"fileUris": fileUris,
|
"fileUris": fileUris,
|
||||||
"mimeTypes": mimeTypes,
|
"mimeTypes": mimeTypes,
|
||||||
|
|
|
@ -4,9 +4,9 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class Map extends StatelessWidget {
|
class Map extends StatelessWidget {
|
||||||
const Map({
|
const Map({
|
||||||
Key key,
|
Key? key,
|
||||||
this.center,
|
required this.center,
|
||||||
this.zoom,
|
required this.zoom,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -44,5 +44,5 @@ class Map extends StatelessWidget {
|
||||||
/// A pair of latitude and longitude coordinates, stored as degrees
|
/// A pair of latitude and longitude coordinates, stored as degrees
|
||||||
final Tuple2<double, double> center;
|
final Tuple2<double, double> center;
|
||||||
final double zoom;
|
final double zoom;
|
||||||
final void Function() onTap;
|
final VoidCallback? onTap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,5 @@ class AndroidItemDownloadSuccessfulNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> fileUris;
|
final List<String> fileUris;
|
||||||
final List<String> mimeTypes;
|
final List<String?> mimeTypes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ class SelfSignedCertManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_BadCertInfo _latestBadCert;
|
late _BadCertInfo _latestBadCert;
|
||||||
var _whitelist = <_CertInfo>[];
|
var _whitelist = <_CertInfo>[];
|
||||||
|
|
||||||
static SelfSignedCertManager _inst = SelfSignedCertManager._();
|
static SelfSignedCertManager _inst = SelfSignedCertManager._();
|
||||||
|
@ -168,7 +168,7 @@ class _BadCertInfo {
|
||||||
|
|
||||||
class _CustomHttpOverrides extends HttpOverrides {
|
class _CustomHttpOverrides extends HttpOverrides {
|
||||||
@override
|
@override
|
||||||
HttpClient createHttpClient(SecurityContext context) {
|
HttpClient createHttpClient(SecurityContext? context) {
|
||||||
return super.createHttpClient(context)
|
return super.createHttpClient(context)
|
||||||
..badCertificateCallback = (cert, host, port) {
|
..badCertificateCallback = (cert, host, port) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -10,5 +10,5 @@ class AndroidShare extends itf.Share {
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> fileUris;
|
final List<String> fileUris;
|
||||||
final List<String> mimeTypes;
|
final List<String?> mimeTypes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
class OrNull<T> {
|
class OrNull<T> {
|
||||||
OrNull(this.obj);
|
OrNull(this.obj);
|
||||||
|
|
||||||
static bool isNull(OrNull x) => x != null && x.obj == null;
|
/// Return iff the value of [x] is set to null, which means if [x] itself is
|
||||||
|
/// null, false will still be returned
|
||||||
|
static bool isSetNull(OrNull? x) => x != null && x.obj == null;
|
||||||
|
|
||||||
final T obj;
|
final T? obj;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,13 @@ abstract class UniversalStorage {
|
||||||
|
|
||||||
/// Return the content associated with [name], or null if no such association
|
/// Return the content associated with [name], or null if no such association
|
||||||
/// exists
|
/// exists
|
||||||
Future<Uint8List> getBinary(String name);
|
Future<Uint8List?> getBinary(String name);
|
||||||
|
|
||||||
Future<void> putString(String name, String content);
|
Future<void> putString(String name, String content);
|
||||||
|
|
||||||
/// Return the string associated with [name], or null if no such association
|
/// Return the string associated with [name], or null if no such association
|
||||||
/// exists
|
/// exists
|
||||||
Future<String> getString(String name);
|
Future<String?> getString(String name);
|
||||||
|
|
||||||
Future<void> remove(String name);
|
Future<void> remove(String name);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,67 +12,65 @@ class Pref {
|
||||||
|
|
||||||
factory Pref.inst() => _inst;
|
factory Pref.inst() => _inst;
|
||||||
|
|
||||||
List<Account> getAccounts([List<Account> def]) {
|
List<Account>? getAccounts() {
|
||||||
final jsonObjs = _pref.getStringList("accounts");
|
final jsonObjs = _pref.getStringList("accounts");
|
||||||
return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e)))?.toList() ??
|
return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e))).toList();
|
||||||
def;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Account> getAccountsOr(List<Account> def) => getAccounts() ?? def;
|
||||||
Future<bool> setAccounts(List<Account> value) {
|
Future<bool> setAccounts(List<Account> value) {
|
||||||
final jsons = value.map((e) => jsonEncode(e.toJson())).toList();
|
final jsons = value.map((e) => jsonEncode(e.toJson())).toList();
|
||||||
return _pref.setStringList("accounts", jsons);
|
return _pref.setStringList("accounts", jsons);
|
||||||
}
|
}
|
||||||
|
|
||||||
int getCurrentAccountIndex([int def]) =>
|
int? getCurrentAccountIndex() => _pref.getInt("currentAccountIndex");
|
||||||
_pref.getInt("currentAccountIndex") ?? def;
|
int getCurrentAccountIndexOr(int def) => getCurrentAccountIndex() ?? def;
|
||||||
|
|
||||||
Future<bool> setCurrentAccountIndex(int value) =>
|
Future<bool> setCurrentAccountIndex(int value) =>
|
||||||
_pref.setInt("currentAccountIndex", value);
|
_pref.setInt("currentAccountIndex", value);
|
||||||
|
|
||||||
int getHomePhotosZoomLevel([int def]) =>
|
int? getHomePhotosZoomLevel() => _pref.getInt("homePhotosZoomLevel");
|
||||||
_pref.getInt("homePhotosZoomLevel") ?? def;
|
int getHomePhotosZoomLevelOr(int def) => getHomePhotosZoomLevel() ?? def;
|
||||||
|
|
||||||
Future<bool> setHomePhotosZoomLevel(int value) =>
|
Future<bool> setHomePhotosZoomLevel(int value) =>
|
||||||
_pref.setInt("homePhotosZoomLevel", value);
|
_pref.setInt("homePhotosZoomLevel", value);
|
||||||
|
|
||||||
int getAlbumViewerZoomLevel([int def]) =>
|
int? getAlbumViewerZoomLevel() => _pref.getInt("albumViewerZoomLevel");
|
||||||
_pref.getInt("albumViewerZoomLevel") ?? def;
|
int getAlbumViewerZoomLevelOr(int def) => getAlbumViewerZoomLevel() ?? def;
|
||||||
|
|
||||||
Future<bool> setAlbumViewerZoomLevel(int value) =>
|
Future<bool> setAlbumViewerZoomLevel(int value) =>
|
||||||
_pref.setInt("albumViewerZoomLevel", value);
|
_pref.setInt("albumViewerZoomLevel", value);
|
||||||
|
|
||||||
bool isEnableExif([bool def = true]) => _pref.getBool("isEnableExif") ?? def;
|
bool? isEnableExif() => _pref.getBool("isEnableExif");
|
||||||
|
bool isEnableExifOr([bool def = true]) => isEnableExif() ?? def;
|
||||||
Future<bool> setEnableExif(bool value) =>
|
Future<bool> setEnableExif(bool value) =>
|
||||||
_pref.setBool("isEnableExif", value);
|
_pref.setBool("isEnableExif", value);
|
||||||
|
|
||||||
int getSetupProgress([int def = 0]) => _pref.getInt("setupProgress") ?? def;
|
int? getSetupProgress() => _pref.getInt("setupProgress");
|
||||||
|
int getSetupProgressOr([int def = 0]) => getSetupProgress() ?? def;
|
||||||
Future<bool> setSetupProgress(int value) =>
|
Future<bool> setSetupProgress(int value) =>
|
||||||
_pref.setInt("setupProgress", value);
|
_pref.setInt("setupProgress", value);
|
||||||
|
|
||||||
/// Return the version number when the app last ran
|
/// Return the version number when the app last ran
|
||||||
int getLastVersion([int def = 0]) => _pref.getInt("lastVersion") ?? def;
|
int? getLastVersion() => _pref.getInt("lastVersion");
|
||||||
|
int getLastVersionOr(int def) => getLastVersion() ?? def;
|
||||||
Future<bool> setLastVersion(int value) => _pref.setInt("lastVersion", value);
|
Future<bool> setLastVersion(int value) => _pref.setInt("lastVersion", value);
|
||||||
|
|
||||||
bool isDarkTheme([bool def = false]) => _pref.getBool("isDarkTheme") ?? def;
|
bool? isDarkTheme() => _pref.getBool("isDarkTheme");
|
||||||
|
bool isDarkThemeOr(bool def) => isDarkTheme() ?? def;
|
||||||
Future<bool> setDarkTheme(bool value) => _pref.setBool("isDarkTheme", value);
|
Future<bool> setDarkTheme(bool value) => _pref.setBool("isDarkTheme", value);
|
||||||
|
|
||||||
int getLanguage([int def = 0]) => _pref.getInt("language") ?? def;
|
int? getLanguage() => _pref.getInt("language");
|
||||||
|
int getLanguageOr(int def) => getLanguage() ?? def;
|
||||||
Future<bool> setLanguage(int value) => _pref.setInt("language", value);
|
Future<bool> setLanguage(int value) => _pref.setInt("language", value);
|
||||||
|
|
||||||
Pref._();
|
Pref._();
|
||||||
|
|
||||||
static final _inst = Pref._();
|
static final _inst = Pref._();
|
||||||
SharedPreferences _pref;
|
late SharedPreferences _pref;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PrefExtension on Pref {
|
extension PrefExtension on Pref {
|
||||||
Account getCurrentAccount() {
|
Account? getCurrentAccount() {
|
||||||
try {
|
try {
|
||||||
return Pref.inst().getAccounts()[Pref.inst().getCurrentAccountIndex()];
|
return Pref.inst().getAccounts()![Pref.inst().getCurrentAccountIndex()!];
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ShareHandler {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ProcessingDialog(
|
builder: (context) => ProcessingDialog(
|
||||||
text: AppLocalizations.of(context).shareDownloadingDialogContent),
|
text: AppLocalizations.of(context)!.shareDownloadingDialogContent),
|
||||||
);
|
);
|
||||||
final results = <Tuple2<File, dynamic>>[];
|
final results = <Tuple2<File, dynamic>>[];
|
||||||
for (final f in files) {
|
for (final f in files) {
|
||||||
|
@ -33,7 +33,7 @@ class ShareHandler {
|
||||||
} on PermissionException catch (_) {
|
} on PermissionException catch (_) {
|
||||||
_log.warning("[shareFiles] Permission not granted");
|
_log.warning("[shareFiles] Permission not granted");
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.downloadFailureNoPermissionNotification),
|
.downloadFailureNoPermissionNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
|
|
@ -22,7 +22,7 @@ class SnackBarManager {
|
||||||
/// Show a snack bar if possible
|
/// Show a snack bar if possible
|
||||||
///
|
///
|
||||||
/// If the snack bar can't be shown at this time, return null
|
/// If the snack bar can't be shown at this time, return null
|
||||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
|
||||||
SnackBar snackBar) {
|
SnackBar snackBar) {
|
||||||
for (final h in _handlers.reversed) {
|
for (final h in _handlers.reversed) {
|
||||||
final result = h.showSnackBar(snackBar);
|
final result = h.showSnackBar(snackBar);
|
||||||
|
@ -42,6 +42,6 @@ class SnackBarManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SnackBarHandler {
|
abstract class SnackBarHandler {
|
||||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? showSnackBar(
|
||||||
SnackBar snackBar);
|
SnackBar snackBar);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class AppTheme extends StatelessWidget {
|
class AppTheme extends StatelessWidget {
|
||||||
const AppTheme({@required this.child});
|
const AppTheme({
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -46,24 +48,24 @@ class AppTheme extends StatelessWidget {
|
||||||
|
|
||||||
static Color getSelectionOverlayColor(BuildContext context) {
|
static Color getSelectionOverlayColor(BuildContext context) {
|
||||||
return Theme.of(context).brightness == Brightness.light
|
return Theme.of(context).brightness == Brightness.light
|
||||||
? primarySwatchLight[100].withOpacity(0.7)
|
? primarySwatchLight[100]!.withOpacity(0.7)
|
||||||
: primarySwatchDark[700].withOpacity(0.7);
|
: primarySwatchDark[700]!.withOpacity(0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color getSelectionCheckColor(BuildContext context) {
|
static Color getSelectionCheckColor(BuildContext context) {
|
||||||
return Theme.of(context).brightness == Brightness.light
|
return Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[800]
|
? Colors.grey[800]!
|
||||||
: Colors.grey[350];
|
: Colors.grey[350]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color getOverscrollIndicatorColor(BuildContext context) {
|
static Color getOverscrollIndicatorColor(BuildContext context) {
|
||||||
return Theme.of(context).brightness == Brightness.light
|
return Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[800]
|
? Colors.grey[800]!
|
||||||
: Colors.grey[200];
|
: Colors.grey[200]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color getRootPickerContentBoxColor(BuildContext context) {
|
static Color getRootPickerContentBoxColor(BuildContext context) {
|
||||||
return Colors.blue[200];
|
return Colors.blue[200]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color getPrimaryTextColor(BuildContext context) {
|
static Color getPrimaryTextColor(BuildContext context) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import 'package:nc_photos/use_case/remove.dart';
|
||||||
/// doing it on every query
|
/// doing it on every query
|
||||||
class TouchTokenManager {
|
class TouchTokenManager {
|
||||||
Future<void> setRemoteToken(
|
Future<void> setRemoteToken(
|
||||||
FileRepo fileRepo, Account account, File file, String token) async {
|
FileRepo fileRepo, Account account, File file, String? token) async {
|
||||||
_log.info(
|
_log.info(
|
||||||
"[setRemoteToken] Set remote token for file '${file.path}': $token");
|
"[setRemoteToken] Set remote token for file '${file.path}': $token");
|
||||||
final path = _getRemotePath(account, file);
|
final path = _getRemotePath(account, file);
|
||||||
|
@ -36,7 +36,7 @@ class TouchTokenManager {
|
||||||
|
|
||||||
/// Return the touch token for [file] from remote source, or null if no such
|
/// Return the touch token for [file] from remote source, or null if no such
|
||||||
/// file
|
/// file
|
||||||
Future<String> getRemoteToken(
|
Future<String?> getRemoteToken(
|
||||||
FileRepo fileRepo, Account account, File file) async {
|
FileRepo fileRepo, Account account, File file) async {
|
||||||
final path = _getRemotePath(account, file);
|
final path = _getRemotePath(account, file);
|
||||||
try {
|
try {
|
||||||
|
@ -51,7 +51,7 @@ class TouchTokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setLocalToken(Account account, File file, String token) {
|
Future<void> setLocalToken(Account account, File file, String? token) {
|
||||||
_log.info(
|
_log.info(
|
||||||
"[setLocalToken] Set local token for file '${file.path}': $token");
|
"[setLocalToken] Set local token for file '${file.path}': $token");
|
||||||
final name = _getLocalStorageName(account, file);
|
final name = _getLocalStorageName(account, file);
|
||||||
|
@ -62,7 +62,7 @@ class TouchTokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getLocalToken(Account account, File file) async {
|
Future<String?> getLocalToken(Account account, File file) async {
|
||||||
final name = _getLocalStorageName(account, file);
|
final name = _getLocalStorageName(account, file);
|
||||||
return platform.UniversalStorage().getString(name);
|
return platform.UniversalStorage().getString(name);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,9 @@ class LoadMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _loadMetadata({
|
Future<Map<String, dynamic>> _loadMetadata({
|
||||||
@required File file,
|
required File file,
|
||||||
exifdart.AbstractBlobReader Function() exifdartReaderBuilder,
|
required exifdart.AbstractBlobReader Function() exifdartReaderBuilder,
|
||||||
AsyncImageInput Function() imageSizeGetterInputBuilder,
|
required AsyncImageInput Function() imageSizeGetterInputBuilder,
|
||||||
}) async {
|
}) async {
|
||||||
var metadata = exifdart.Metadata();
|
var metadata = exifdart.Metadata();
|
||||||
if (file_util.isMetadataSupportedFormat(file)) {
|
if (file_util.isMetadataSupportedFormat(file)) {
|
||||||
|
@ -47,8 +47,8 @@ class LoadMetadata {
|
||||||
await AsyncImageSizeGetter.getSize(imageSizeGetterInputBuilder());
|
await AsyncImageSizeGetter.getSize(imageSizeGetterInputBuilder());
|
||||||
// image size getter doesn't handle exif orientation
|
// image size getter doesn't handle exif orientation
|
||||||
if (metadata.exif?.containsKey("Orientation") == true &&
|
if (metadata.exif?.containsKey("Orientation") == true &&
|
||||||
metadata.exif["Orientation"] >= 5 &&
|
metadata.exif!["Orientation"] >= 5 &&
|
||||||
metadata.exif["Orientation"] <= 8) {
|
metadata.exif!["Orientation"] <= 8) {
|
||||||
// 90 deg CW/CCW
|
// 90 deg CW/CCW
|
||||||
imageWidth = resolution.height;
|
imageWidth = resolution.height;
|
||||||
imageHeight = resolution.width;
|
imageHeight = resolution.width;
|
||||||
|
@ -66,12 +66,12 @@ class LoadMetadata {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (metadata.rotateAngleCcw != null &&
|
if (metadata.rotateAngleCcw != null &&
|
||||||
metadata.rotateAngleCcw % 180 != 0) {
|
metadata.rotateAngleCcw! % 180 != 0) {
|
||||||
imageWidth = metadata.imageHeight;
|
imageWidth = metadata.imageHeight!;
|
||||||
imageHeight = metadata.imageWidth;
|
imageHeight = metadata.imageWidth!;
|
||||||
} else {
|
} else {
|
||||||
imageWidth = metadata.imageWidth;
|
imageWidth = metadata.imageWidth!;
|
||||||
imageHeight = metadata.imageHeight;
|
imageHeight = metadata.imageHeight!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ class Remove {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cleanUpAlbums(Account account, File file) async {
|
Future<void> _cleanUpAlbums(Account account, File file) async {
|
||||||
|
final albumRepo = this.albumRepo!;
|
||||||
final albums = (await ListAlbum(fileRepo, albumRepo)(account)
|
final albums = (await ListAlbum(fileRepo, albumRepo)(account)
|
||||||
.where((event) => event is Album)
|
.where((event) => event is Album)
|
||||||
.toList()).cast<Album>();
|
.toList()).cast<Album>();
|
||||||
|
@ -58,7 +59,7 @@ class Remove {
|
||||||
}
|
}
|
||||||
|
|
||||||
final FileRepo fileRepo;
|
final FileRepo fileRepo;
|
||||||
final AlbumRepo albumRepo;
|
final AlbumRepo? albumRepo;
|
||||||
|
|
||||||
static final _log = Logger("use_case.remove.Remove");
|
static final _log = Logger("use_case.remove.Remove");
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,18 +44,22 @@ class ResyncAlbum {
|
||||||
|
|
||||||
Future<AlbumFileItem> _syncOne(Account account, AlbumFileItem item,
|
Future<AlbumFileItem> _syncOne(Account account, AlbumFileItem item,
|
||||||
ObjectStore objStore, Index index) async {
|
ObjectStore objStore, Index index) async {
|
||||||
Map dbItem;
|
Map? dbItem;
|
||||||
if (item.file.fileId != null) {
|
if (item.file.fileId != null) {
|
||||||
final List dbItems = await index
|
final List dbItems = await index
|
||||||
.getAll(AppDbFileDbEntry.toNamespacedFileId(account, item.file));
|
.getAll(AppDbFileDbEntry.toNamespacedFileId(account, item.file));
|
||||||
// find the one owned by us
|
// find the one owned by us
|
||||||
dbItem = dbItems.firstWhere((element) {
|
try {
|
||||||
final e = AppDbFileDbEntry.fromJson(element.cast<String, dynamic>());
|
dbItem = dbItems.firstWhere((element) {
|
||||||
return file_util.getUserDirName(e.file) == account.username;
|
final e = AppDbFileDbEntry.fromJson(element.cast<String, dynamic>());
|
||||||
}, orElse: () => null);
|
return file_util.getUserDirName(e.file) == account.username;
|
||||||
|
});
|
||||||
|
} on StateError catch (_) {
|
||||||
|
// not found
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dbItem = await objStore
|
dbItem = await objStore
|
||||||
.getObject(AppDbFileDbEntry.toPrimaryKey(account, item.file));
|
.getObject(AppDbFileDbEntry.toPrimaryKey(account, item.file)) as Map;
|
||||||
}
|
}
|
||||||
if (dbItem == null) {
|
if (dbItem == null) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
|
|
|
@ -40,8 +40,9 @@ class UpdateDynamicAlbumCover {
|
||||||
}
|
}
|
||||||
|
|
||||||
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
|
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
|
||||||
final coverFile = sortedFiles.firstWhere((element) => element.hasPreview);
|
try {
|
||||||
if (coverFile != null) {
|
final coverFile =
|
||||||
|
sortedFiles.firstWhere((element) => element.hasPreview ?? false);
|
||||||
// cache the result for later use
|
// cache the result for later use
|
||||||
if (coverFile.path !=
|
if (coverFile.path !=
|
||||||
(album.coverProvider as AlbumAutoCoverProvider).coverFile?.path) {
|
(album.coverProvider as AlbumAutoCoverProvider).coverFile?.path) {
|
||||||
|
@ -51,6 +52,8 @@ class UpdateDynamicAlbumCover {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} on StateError catch (_) {
|
||||||
|
// no files
|
||||||
}
|
}
|
||||||
return album;
|
return album;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ class UpdateDynamicAlbumTime {
|
||||||
}
|
}
|
||||||
|
|
||||||
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
|
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
|
||||||
DateTime latestItemTime;
|
DateTime? latestItemTime;
|
||||||
try {
|
try {
|
||||||
latestItemTime = sortedFiles.first.bestDateTime;
|
latestItemTime = sortedFiles.first.bestDateTime;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
|
@ -40,8 +40,8 @@ class UpdateMissingMetadata {
|
||||||
_log.fine("[call] Updating metadata for ${file.path}");
|
_log.fine("[call] Updating metadata for ${file.path}");
|
||||||
final binary = await GetFileBinary(fileRepo)(account, file);
|
final binary = await GetFileBinary(fileRepo)(account, file);
|
||||||
final metadata = await LoadMetadata()(account, file, binary);
|
final metadata = await LoadMetadata()(account, file, binary);
|
||||||
int imageWidth, imageHeight;
|
int? imageWidth, imageHeight;
|
||||||
Exif exif;
|
Exif? exif;
|
||||||
if (metadata.containsKey("resolution")) {
|
if (metadata.containsKey("resolution")) {
|
||||||
imageWidth = metadata["resolution"]["width"];
|
imageWidth = metadata["resolution"]["width"];
|
||||||
imageHeight = metadata["resolution"]["height"];
|
imageHeight = metadata["resolution"]["height"];
|
||||||
|
|
|
@ -12,9 +12,9 @@ class UpdateProperty {
|
||||||
Future<void> call(
|
Future<void> call(
|
||||||
Account account,
|
Account account,
|
||||||
File file, {
|
File file, {
|
||||||
OrNull<Metadata> metadata,
|
OrNull<Metadata>? metadata,
|
||||||
OrNull<bool> isArchived,
|
OrNull<bool>? isArchived,
|
||||||
OrNull<DateTime> overrideDateTime,
|
OrNull<DateTime>? overrideDateTime,
|
||||||
}) async {
|
}) async {
|
||||||
if (metadata == null && isArchived == null && overrideDateTime == null) {
|
if (metadata == null && isArchived == null && overrideDateTime == null) {
|
||||||
// ?
|
// ?
|
||||||
|
@ -22,9 +22,9 @@ class UpdateProperty {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata?.obj != null && metadata.obj.fileEtag != file.etag) {
|
if (metadata?.obj != null && metadata!.obj!.fileEtag != file.etag) {
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj.fileEtag}, file: ${file.etag})");
|
"[call] Metadata fileEtag mismatch with actual file's (metadata: ${metadata.obj!.fileEtag}, file: ${file.etag})");
|
||||||
}
|
}
|
||||||
await fileRepo.updateProperty(
|
await fileRepo.updateProperty(
|
||||||
account,
|
account,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:idb_shim/idb_browser.dart';
|
import 'package:idb_shim/idb_browser.dart';
|
||||||
import 'package:idb_shim/idb_shim.dart';
|
import 'package:idb_shim/idb_shim.dart';
|
||||||
|
|
||||||
IdbFactory getDbFactory() => getIdbFactory();
|
IdbFactory getDbFactory() => idbFactoryBrowser;
|
||||||
|
|
|
@ -8,9 +8,9 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class Map extends StatefulWidget {
|
class Map extends StatefulWidget {
|
||||||
const Map({
|
const Map({
|
||||||
Key key,
|
Key? key,
|
||||||
this.center,
|
required this.center,
|
||||||
this.zoom,
|
required this.zoom,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class Map extends StatefulWidget {
|
||||||
/// A pair of latitude and longitude coordinates, stored as degrees
|
/// A pair of latitude and longitude coordinates, stored as degrees
|
||||||
final Tuple2<double, double> center;
|
final Tuple2<double, double> center;
|
||||||
final double zoom;
|
final double zoom;
|
||||||
final void Function() onTap;
|
final void Function()? onTap;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MapState extends State<Map> {
|
class _MapState extends State<Map> {
|
||||||
|
|
|
@ -15,8 +15,8 @@ import 'package:nc_photos/widget/sign_in.dart';
|
||||||
/// A dialog that allows the user to switch between accounts
|
/// A dialog that allows the user to switch between accounts
|
||||||
class AccountPickerDialog extends StatefulWidget {
|
class AccountPickerDialog extends StatefulWidget {
|
||||||
AccountPickerDialog({
|
AccountPickerDialog({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -29,7 +29,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_accounts = Pref.inst().getAccounts([]);
|
_accounts = Pref.inst().getAccountsOr([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -48,7 +48,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
Icons.close,
|
Icons.close,
|
||||||
color: AppTheme.getSecondaryTextColor(context),
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
),
|
),
|
||||||
tooltip: AppLocalizations.of(context).deleteTooltip,
|
tooltip: AppLocalizations.of(context)!.deleteTooltip,
|
||||||
onPressed: () => _onRemoveItemPressed(a),
|
onPressed: () => _onRemoveItemPressed(a),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -63,7 +63,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
..pushNamed(SignIn.routeName);
|
..pushNamed(SignIn.routeName);
|
||||||
},
|
},
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: AppLocalizations.of(context).addServerTooltip,
|
message: AppLocalizations.of(context)!.addServerTooltip,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
|
@ -89,7 +89,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
Icons.edit,
|
Icons.edit,
|
||||||
color: AppTheme.getSecondaryTextColor(context),
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
),
|
),
|
||||||
tooltip: AppLocalizations.of(context).editTooltip,
|
tooltip: AppLocalizations.of(context)!.editTooltip,
|
||||||
onPressed: () => _onEditPressed(),
|
onPressed: () => _onEditPressed(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -106,20 +106,28 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onRemoveItemPressed(Account account) {
|
void _onRemoveItemPressed(Account account) {
|
||||||
_removeAccount(account);
|
try {
|
||||||
setState(() {
|
_removeAccount(account);
|
||||||
_accounts = Pref.inst().getAccounts([]);
|
setState(() {
|
||||||
});
|
_accounts = Pref.inst().getAccounts()!;
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
});
|
||||||
content: Text(AppLocalizations.of(context)
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
.removeServerSuccessNotification(account.url)),
|
content: Text(AppLocalizations.of(context)!
|
||||||
duration: k.snackBarDurationNormal,
|
.removeServerSuccessNotification(account.url)),
|
||||||
));
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(e, context)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEditPressed() async {
|
void _onEditPressed() async {
|
||||||
try {
|
try {
|
||||||
final result = await Navigator.of(context).pushNamed(RootPicker.routeName,
|
final result = await Navigator.of(context).pushNamed<Account>(
|
||||||
|
RootPicker.routeName,
|
||||||
arguments: RootPickerArguments(widget.account));
|
arguments: RootPickerArguments(widget.account));
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
// we've got a good account
|
// we've got a good account
|
||||||
|
@ -129,19 +137,19 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final accounts = Pref.inst().getAccounts([]);
|
final accounts = Pref.inst().getAccounts()!;
|
||||||
if (accounts.contains(result)) {
|
if (accounts.contains(result)) {
|
||||||
// conflict with another account. This normally won't happen because
|
// conflict with another account. This normally won't happen because
|
||||||
// the app passwords are unique to each entry, but just in case
|
// the app passwords are unique to each entry, but just in case
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.editAccountConflictFailureNotification),
|
.editAccountConflictFailureNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
accounts[Pref.inst().getCurrentAccountIndex()] = result;
|
accounts[Pref.inst().getCurrentAccountIndex()!] = result;
|
||||||
Pref.inst()..setAccounts(accounts);
|
Pref.inst()..setAccounts(accounts);
|
||||||
Navigator.pushNamedAndRemoveUntil(
|
Navigator.pushNamedAndRemoveUntil(
|
||||||
context, Home.routeName, (route) => false,
|
context, Home.routeName, (route) => false,
|
||||||
|
@ -158,9 +166,9 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeAccount(Account account) {
|
void _removeAccount(Account account) {
|
||||||
final currentAccounts = Pref.inst().getAccounts([]);
|
final currentAccounts = Pref.inst().getAccounts()!;
|
||||||
final currentAccount =
|
final currentAccount =
|
||||||
currentAccounts[Pref.inst().getCurrentAccountIndex()];
|
currentAccounts[Pref.inst().getCurrentAccountIndex()!];
|
||||||
final newAccounts =
|
final newAccounts =
|
||||||
currentAccounts.where((element) => element != account).toList();
|
currentAccounts.where((element) => element != account).toList();
|
||||||
final newAccountIndex = newAccounts.indexOf(currentAccount);
|
final newAccountIndex = newAccounts.indexOf(currentAccount);
|
||||||
|
@ -172,7 +180,7 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
..setCurrentAccountIndex(newAccountIndex);
|
..setCurrentAccountIndex(newAccountIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Account> _accounts;
|
late List<Account> _accounts;
|
||||||
|
|
||||||
static final _log =
|
static final _log =
|
||||||
Logger("widget.account_picker_dialog._AccountPickerDialogState");
|
Logger("widget.account_picker_dialog._AccountPickerDialogState");
|
||||||
|
|
|
@ -21,12 +21,17 @@ class AlbumDirPickerArguments {
|
||||||
class AlbumDirPicker extends StatefulWidget {
|
class AlbumDirPicker extends StatefulWidget {
|
||||||
static const routeName = "/album-dir-picker";
|
static const routeName = "/album-dir-picker";
|
||||||
|
|
||||||
|
static Route buildRoute(AlbumDirPickerArguments args) =>
|
||||||
|
MaterialPageRoute<List<File>>(
|
||||||
|
builder: (context) => AlbumDirPicker.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
AlbumDirPicker({
|
AlbumDirPicker({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AlbumDirPicker.fromArgs(AlbumDirPickerArguments args, {Key key})
|
AlbumDirPicker.fromArgs(AlbumDirPickerArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -81,7 +86,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).albumDirPickerHeaderText,
|
AppLocalizations.of(context)!.albumDirPickerHeaderText,
|
||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -89,7 +94,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
||||||
Align(
|
Align(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).albumDirPickerSubHeaderText,
|
AppLocalizations.of(context)!.albumDirPickerSubHeaderText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -112,7 +117,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => _onConfirmPressed(context),
|
onPressed: () => _onConfirmPressed(context),
|
||||||
child: Text(AppLocalizations.of(context).confirmButtonLabel),
|
child: Text(AppLocalizations.of(context)!.confirmButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -127,7 +132,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker>
|
||||||
if (picked.isEmpty) {
|
if (picked.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).albumDirPickerListEmptyNotification),
|
AppLocalizations.of(context)!.albumDirPickerListEmptyNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,9 +5,9 @@ import 'package:nc_photos/widget/selectable.dart';
|
||||||
|
|
||||||
class AlbumGridItem extends StatelessWidget {
|
class AlbumGridItem extends StatelessWidget {
|
||||||
AlbumGridItem({
|
AlbumGridItem({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.cover,
|
required this.cover,
|
||||||
@required this.title,
|
required this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
this.subtitle2,
|
this.subtitle2,
|
||||||
this.icon,
|
this.icon,
|
||||||
|
@ -50,8 +50,8 @@ class AlbumGridItem extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
title ?? "",
|
title,
|
||||||
style: Theme.of(context).textTheme.bodyText1.copyWith(
|
style: Theme.of(context).textTheme.bodyText1!.copyWith(
|
||||||
color: AppTheme.getPrimaryTextColor(context),
|
color: AppTheme.getPrimaryTextColor(context),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
|
@ -64,7 +64,7 @@ class AlbumGridItem extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
subtitle ?? "",
|
subtitle ?? "",
|
||||||
style: Theme.of(context).textTheme.bodyText2.copyWith(
|
style: Theme.of(context).textTheme.bodyText2!.copyWith(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AppTheme.getSecondaryTextColor(context),
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
),
|
),
|
||||||
|
@ -75,8 +75,8 @@ class AlbumGridItem extends StatelessWidget {
|
||||||
),
|
),
|
||||||
if (subtitle2?.isNotEmpty == true)
|
if (subtitle2?.isNotEmpty == true)
|
||||||
Text(
|
Text(
|
||||||
subtitle2,
|
subtitle2!,
|
||||||
style: Theme.of(context).textTheme.bodyText2.copyWith(
|
style: Theme.of(context).textTheme.bodyText2!.copyWith(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: AppTheme.getSecondaryTextColor(context),
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
),
|
),
|
||||||
|
@ -93,12 +93,12 @@ class AlbumGridItem extends StatelessWidget {
|
||||||
|
|
||||||
final Widget cover;
|
final Widget cover;
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
/// Appears after [subtitle], aligned to the end side of parent
|
/// Appears after [subtitle], aligned to the end side of parent
|
||||||
final String subtitle2;
|
final String? subtitle2;
|
||||||
final IconData icon;
|
final IconData? icon;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,12 +34,16 @@ class AlbumImporterArguments {
|
||||||
class AlbumImporter extends StatefulWidget {
|
class AlbumImporter extends StatefulWidget {
|
||||||
static const routeName = "/album-importer";
|
static const routeName = "/album-importer";
|
||||||
|
|
||||||
|
static Route buildRoute(AlbumImporterArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => AlbumImporter.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
AlbumImporter({
|
AlbumImporter({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AlbumImporter.fromArgs(AlbumImporterArguments args, {Key key})
|
AlbumImporter.fromArgs(AlbumImporterArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -98,7 +102,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).albumImporterHeaderText,
|
AppLocalizations.of(context)!.albumImporterHeaderText,
|
||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -106,7 +110,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
||||||
Align(
|
Align(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).albumImporterSubHeaderText,
|
AppLocalizations.of(context)!.albumImporterSubHeaderText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -141,7 +145,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => _onImportPressed(context),
|
onPressed: () => _onImportPressed(context),
|
||||||
child: Text(AppLocalizations.of(context).importButtonLabel),
|
child: Text(AppLocalizations.of(context)!.importButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -214,7 +218,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ProcessingDialog(
|
builder: (context) => ProcessingDialog(
|
||||||
text: AppLocalizations.of(context).albumImporterProgressText),
|
text: AppLocalizations.of(context)!.albumImporterProgressText),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await _createAllAlbums(context);
|
await _createAllAlbums(context);
|
||||||
|
@ -269,7 +273,7 @@ class _AlbumImporterState extends State<AlbumImporter> {
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
ListImportableAlbumBloc _bloc;
|
late ListImportableAlbumBloc _bloc;
|
||||||
|
|
||||||
var _backingFiles = <File>[];
|
var _backingFiles = <File>[];
|
||||||
final _picks = <File>[];
|
final _picks = <File>[];
|
||||||
|
|
|
@ -18,8 +18,8 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class AlbumPickerDialog extends StatefulWidget {
|
class AlbumPickerDialog extends StatefulWidget {
|
||||||
AlbumPickerDialog({
|
AlbumPickerDialog({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -68,7 +68,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
|
||||||
_reqQuery();
|
_reqQuery();
|
||||||
} else {
|
} else {
|
||||||
// process the current state
|
// process the current state
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onStateChange(context, _bloc.state);
|
_onStateChange(context, _bloc.state);
|
||||||
});
|
});
|
||||||
|
@ -81,7 +81,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
|
||||||
SimpleDialogOption(
|
SimpleDialogOption(
|
||||||
onPressed: () => _onNewAlbumPressed(context),
|
onPressed: () => _onNewAlbumPressed(context),
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: AppLocalizations.of(context).createAlbumTooltip,
|
message: AppLocalizations.of(context)!.createAlbumTooltip,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
|
@ -170,7 +170,7 @@ class _AlbumPickerDialogState extends State<AlbumPickerDialog> {
|
||||||
_bloc.add(ListAlbumBlocQuery(widget.account));
|
_bloc.add(ListAlbumBlocQuery(widget.account));
|
||||||
}
|
}
|
||||||
|
|
||||||
ListAlbumBloc _bloc;
|
late ListAlbumBloc _bloc;
|
||||||
|
|
||||||
final _items = <Album>[];
|
final _items = <Album>[];
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
||||||
AlbumSearchDelegate(BuildContext context, this.account)
|
AlbumSearchDelegate(BuildContext context, this.account)
|
||||||
: super(
|
: super(
|
||||||
searchFieldLabel:
|
searchFieldLabel:
|
||||||
AppLocalizations.of(context).albumSearchTextFieldHint,
|
AppLocalizations.of(context)!.albumSearchTextFieldHint,
|
||||||
) {
|
) {
|
||||||
final fileRepo = FileRepo(FileCachedDataSource());
|
final fileRepo = FileRepo(FileCachedDataSource());
|
||||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||||
|
@ -38,7 +38,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
||||||
return [
|
return [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.clear),
|
icon: Icon(Icons.clear),
|
||||||
tooltip: AppLocalizations.of(context).clearTooltip,
|
tooltip: AppLocalizations.of(context)!.clearTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
query = "";
|
query = "";
|
||||||
},
|
},
|
||||||
|
@ -88,7 +88,7 @@ class AlbumSearchDelegate extends SearchDelegate {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).listNoResultsText,
|
AppLocalizations.of(context)!.listNoResultsText,
|
||||||
style: const TextStyle(fontSize: 24),
|
style: const TextStyle(fontSize: 24),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -42,13 +42,17 @@ class AlbumViewerArguments {
|
||||||
class AlbumViewer extends StatefulWidget {
|
class AlbumViewer extends StatefulWidget {
|
||||||
static const routeName = "/album-viewer";
|
static const routeName = "/album-viewer";
|
||||||
|
|
||||||
|
static Route buildRoute(AlbumViewerArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => AlbumViewer.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
AlbumViewer({
|
AlbumViewer({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.album,
|
required this.album,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
AlbumViewer.fromArgs(AlbumViewerArguments args, {Key key})
|
AlbumViewer.fromArgs(AlbumViewerArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -96,7 +100,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
@override
|
@override
|
||||||
enterEditMode() {
|
enterEditMode() {
|
||||||
super.enterEditMode();
|
super.enterEditMode();
|
||||||
_editAlbum = _album.copyWith();
|
_editAlbum = _album!.copyWith();
|
||||||
setState(() {
|
setState(() {
|
||||||
_transformItems();
|
_transformItems();
|
||||||
});
|
});
|
||||||
|
@ -104,7 +108,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
if (!SessionStorage().hasShowDragRearrangeNotification) {
|
if (!SessionStorage().hasShowDragRearrangeNotification) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).albumEditDragRearrangeNotification),
|
AppLocalizations.of(context)!.albumEditDragRearrangeNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
SessionStorage().hasShowDragRearrangeNotification = true;
|
SessionStorage().hasShowDragRearrangeNotification = true;
|
||||||
|
@ -112,15 +116,15 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
validateEditMode() => _editFormKey?.currentState?.validate() == true;
|
validateEditMode() => _editFormKey.currentState?.validate() == true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
doneEditMode() {
|
doneEditMode() {
|
||||||
try {
|
try {
|
||||||
// persist the changes
|
// persist the changes
|
||||||
_editFormKey.currentState.save();
|
_editFormKey.currentState!.save();
|
||||||
final newAlbum = makeEdited(_editAlbum);
|
final newAlbum = makeEdited(_editAlbum!);
|
||||||
if (newAlbum.copyWith(lastUpdated: OrNull(_album.lastUpdated)) !=
|
if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
|
||||||
_album) {
|
_album) {
|
||||||
_log.info("[doneEditMode] Album modified: $newAlbum");
|
_log.info("[doneEditMode] Album modified: $newAlbum");
|
||||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||||
|
@ -217,7 +221,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
} else if (isSelectionMode) {
|
} else if (isSelectionMode) {
|
||||||
return _buildSelectionAppBar(context);
|
return _buildSelectionAppBar(context);
|
||||||
} else {
|
} else {
|
||||||
return buildNormalAppBar(context, widget.account, _album);
|
return buildNormalAppBar(context, widget.account, _album!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,14 +230,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
if (platform_k.isAndroid)
|
if (platform_k.isAndroid)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_onSelectionAppBarSharePressed(context);
|
_onSelectionAppBarSharePressed(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.remove),
|
icon: const Icon(Icons.remove),
|
||||||
tooltip: AppLocalizations.of(context).removeSelectedFromAlbumTooltip,
|
tooltip: AppLocalizations.of(context)!.removeSelectedFromAlbumTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_onSelectionAppBarRemovePressed();
|
_onSelectionAppBarRemovePressed();
|
||||||
},
|
},
|
||||||
|
@ -245,12 +249,12 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
return buildEditAppBar(context, widget.account, widget.album, actions: [
|
return buildEditAppBar(context, widget.account, widget.album, actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.text_fields),
|
icon: Icon(Icons.text_fields),
|
||||||
tooltip: AppLocalizations.of(context).albumAddTextTooltip,
|
tooltip: AppLocalizations.of(context)!.albumAddTextTooltip,
|
||||||
onPressed: _onEditAppBarAddTextPressed,
|
onPressed: _onEditAppBarAddTextPressed,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.sort_by_alpha),
|
icon: Icon(Icons.sort_by_alpha),
|
||||||
tooltip: AppLocalizations.of(context).sortTooltip,
|
tooltip: AppLocalizations.of(context)!.sortTooltip,
|
||||||
onPressed: _onEditAppBarSortPressed,
|
onPressed: _onEditAppBarSortPressed,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
@ -279,7 +283,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
if (selected.isEmpty) {
|
if (selected.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content:
|
||||||
Text(AppLocalizations.of(context).shareSelectedEmptyNotification),
|
Text(AppLocalizations.of(context)!.shareSelectedEmptyNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
|
@ -301,14 +305,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||||
final newAlbum = _album.copyWith(
|
final newAlbum = _album!.copyWith(
|
||||||
provider: AlbumStaticProvider(
|
provider: AlbumStaticProvider(
|
||||||
items: newItems,
|
items: newItems,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
UpdateAlbum(albumRepo)(widget.account, newAlbum).then((_) {
|
UpdateAlbum(albumRepo)(widget.account, newAlbum).then((_) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.removeSelectedFromAlbumSuccessNotification(
|
.removeSelectedFromAlbumSuccessNotification(
|
||||||
selectedIndexes.length)),
|
selectedIndexes.length)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
|
@ -323,7 +327,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
stacktrace);
|
stacktrace);
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"${AppLocalizations.of(context).removeSelectedFromAlbumFailureNotification}: "
|
"${AppLocalizations.of(context)!.removeSelectedFromAlbumFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -334,14 +338,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEditAppBarSortPressed() {
|
void _onEditAppBarSortPressed() {
|
||||||
final sortProvider = _editAlbum.sortProvider;
|
final sortProvider = _editAlbum!.sortProvider;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => FancyOptionPicker(
|
builder: (context) => FancyOptionPicker(
|
||||||
title: AppLocalizations.of(context).sortOptionDialogTitle,
|
title: AppLocalizations.of(context)!.sortOptionDialogTitle,
|
||||||
items: [
|
items: [
|
||||||
FancyOptionPickerItem(
|
FancyOptionPickerItem(
|
||||||
label: AppLocalizations.of(context).sortOptionTimeAscendingLabel,
|
label: AppLocalizations.of(context)!.sortOptionTimeAscendingLabel,
|
||||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||||
sortProvider.isAscending,
|
sortProvider.isAscending,
|
||||||
onSelect: () {
|
onSelect: () {
|
||||||
|
@ -350,7 +354,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FancyOptionPickerItem(
|
FancyOptionPickerItem(
|
||||||
label: AppLocalizations.of(context).sortOptionTimeDescendingLabel,
|
label: AppLocalizations.of(context)!.sortOptionTimeDescendingLabel,
|
||||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||||
!sortProvider.isAscending,
|
!sortProvider.isAscending,
|
||||||
onSelect: () {
|
onSelect: () {
|
||||||
|
@ -364,7 +368,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSortOldestPressed() {
|
void _onSortOldestPressed() {
|
||||||
_editAlbum = _editAlbum.copyWith(
|
_editAlbum = _editAlbum!.copyWith(
|
||||||
sortProvider: AlbumTimeSortProvider(isAscending: true),
|
sortProvider: AlbumTimeSortProvider(isAscending: true),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -373,7 +377,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSortNewestPressed() {
|
void _onSortNewestPressed() {
|
||||||
_editAlbum = _editAlbum.copyWith(
|
_editAlbum = _editAlbum!.copyWith(
|
||||||
sortProvider: AlbumTimeSortProvider(isAscending: false),
|
sortProvider: AlbumTimeSortProvider(isAscending: false),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -431,7 +435,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
final newIndex =
|
final newIndex =
|
||||||
toIndex + (isBefore ? 0 : 1) + (fromIndex < toIndex ? -1 : 0);
|
toIndex + (isBefore ? 0 : 1) + (fromIndex < toIndex ? -1 : 0);
|
||||||
_sortedItems.insert(newIndex, item);
|
_sortedItems.insert(newIndex, item);
|
||||||
_editAlbum = _editAlbum.copyWith(
|
_editAlbum = _editAlbum!.copyWith(
|
||||||
sortProvider: AlbumNullSortProvider(),
|
sortProvider: AlbumNullSortProvider(),
|
||||||
// save the current order
|
// save the current order
|
||||||
provider: AlbumStaticProvider(
|
provider: AlbumStaticProvider(
|
||||||
|
@ -444,14 +448,14 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEditAppBarAddTextPressed() {
|
void _onEditAppBarAddTextPressed() {
|
||||||
showDialog(
|
showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => SimpleInputDialog(),
|
builder: (context) => SimpleInputDialog(),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_editAlbum = _editAlbum.copyWith(
|
_editAlbum = _editAlbum!.copyWith(
|
||||||
provider: AlbumStaticProvider(
|
provider: AlbumStaticProvider(
|
||||||
items: [
|
items: [
|
||||||
AlbumLabelItem(text: value),
|
AlbumLabelItem(text: value),
|
||||||
|
@ -466,7 +470,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLabelItemEditPressed(AlbumLabelItem item, int index) {
|
void _onLabelItemEditPressed(AlbumLabelItem item, int index) {
|
||||||
showDialog(
|
showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => SimpleInputDialog(
|
builder: (context) => SimpleInputDialog(
|
||||||
initialText: item.text,
|
initialText: item.text,
|
||||||
|
@ -476,7 +480,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_sortedItems[index] = AlbumLabelItem(text: value);
|
_sortedItems[index] = AlbumLabelItem(text: value);
|
||||||
_editAlbum = _editAlbum.copyWith(
|
_editAlbum = _editAlbum!.copyWith(
|
||||||
provider: AlbumStaticProvider(
|
provider: AlbumStaticProvider(
|
||||||
items: _sortedItems,
|
items: _sortedItems,
|
||||||
),
|
),
|
||||||
|
@ -490,9 +494,10 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
void _transformItems() {
|
void _transformItems() {
|
||||||
if (_editAlbum != null) {
|
if (_editAlbum != null) {
|
||||||
// edit mode
|
// edit mode
|
||||||
_sortedItems = _editAlbum.sortProvider.sort(_getAlbumItemsOf(_editAlbum));
|
_sortedItems =
|
||||||
|
_editAlbum!.sortProvider.sort(_getAlbumItemsOf(_editAlbum!));
|
||||||
} else {
|
} else {
|
||||||
_sortedItems = _album.sortProvider.sort(_getAlbumItemsOf(_album));
|
_sortedItems = _album!.sortProvider.sort(_getAlbumItemsOf(_album!));
|
||||||
}
|
}
|
||||||
_backingFiles = _sortedItems
|
_backingFiles = _sortedItems
|
||||||
.whereType<AlbumFileItem>()
|
.whereType<AlbumFileItem>()
|
||||||
|
@ -644,29 +649,29 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
static List<AlbumItem> _getAlbumItemsOf(Album a) =>
|
static List<AlbumItem> _getAlbumItemsOf(Album a) =>
|
||||||
AlbumStaticProvider.of(a).items;
|
AlbumStaticProvider.of(a).items;
|
||||||
|
|
||||||
Album _album;
|
Album? _album;
|
||||||
var _sortedItems = <AlbumItem>[];
|
var _sortedItems = <AlbumItem>[];
|
||||||
var _backingFiles = <File>[];
|
var _backingFiles = <File>[];
|
||||||
|
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
double _itemListMaxExtent;
|
double? _itemListMaxExtent;
|
||||||
bool _isDragging = false;
|
bool _isDragging = false;
|
||||||
// == null if not drag scrolling
|
// == null if not drag scrolling
|
||||||
bool _isDragScrollingDown;
|
bool? _isDragScrollingDown;
|
||||||
final _editFormKey = GlobalKey<FormState>();
|
final _editFormKey = GlobalKey<FormState>();
|
||||||
Album _editAlbum;
|
Album? _editAlbum;
|
||||||
|
|
||||||
static final _log = Logger("widget.album_viewer._AlbumViewerState");
|
static final _log = Logger("widget.album_viewer._AlbumViewerState");
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _ListItem implements SelectableItem, DraggableItem {
|
abstract class _ListItem implements SelectableItem, DraggableItem {
|
||||||
_ListItem({
|
_ListItem({
|
||||||
@required this.index,
|
required this.index,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
DragTargetAccept<DraggableItem> onDropBefore,
|
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||||
DragTargetAccept<DraggableItem> onDropAfter,
|
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||||
VoidCallback onDragStarted,
|
VoidCallback? onDragStarted,
|
||||||
VoidCallback onDragEndedAny,
|
VoidCallback? onDragEndedAny,
|
||||||
}) : _onTap = onTap,
|
}) : _onTap = onTap,
|
||||||
_onDropBefore = onDropBefore,
|
_onDropBefore = onDropBefore,
|
||||||
_onDropAfter = onDropAfter,
|
_onDropAfter = onDropAfter,
|
||||||
|
@ -709,22 +714,22 @@ abstract class _ListItem implements SelectableItem, DraggableItem {
|
||||||
|
|
||||||
final int index;
|
final int index;
|
||||||
|
|
||||||
final VoidCallback _onTap;
|
final VoidCallback? _onTap;
|
||||||
final DragTargetAccept<DraggableItem> _onDropBefore;
|
final DragTargetAccept<DraggableItem>? _onDropBefore;
|
||||||
final DragTargetAccept<DraggableItem> _onDropAfter;
|
final DragTargetAccept<DraggableItem>? _onDropAfter;
|
||||||
final VoidCallback _onDragStarted;
|
final VoidCallback? _onDragStarted;
|
||||||
final VoidCallback _onDragEndedAny;
|
final VoidCallback? _onDragEndedAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _FileListItem extends _ListItem {
|
abstract class _FileListItem extends _ListItem {
|
||||||
_FileListItem({
|
_FileListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required this.file,
|
required this.file,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
DragTargetAccept<DraggableItem> onDropBefore,
|
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||||
DragTargetAccept<DraggableItem> onDropAfter,
|
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||||
VoidCallback onDragStarted,
|
VoidCallback? onDragStarted,
|
||||||
VoidCallback onDragEndedAny,
|
VoidCallback? onDragEndedAny,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
@ -739,15 +744,15 @@ abstract class _FileListItem extends _ListItem {
|
||||||
|
|
||||||
class _ImageListItem extends _FileListItem {
|
class _ImageListItem extends _FileListItem {
|
||||||
_ImageListItem({
|
_ImageListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
DragTargetAccept<DraggableItem> onDropBefore,
|
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||||
DragTargetAccept<DraggableItem> onDropAfter,
|
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||||
VoidCallback onDragStarted,
|
VoidCallback? onDragStarted,
|
||||||
VoidCallback onDragEndedAny,
|
VoidCallback? onDragEndedAny,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
file: file,
|
file: file,
|
||||||
|
@ -773,15 +778,15 @@ class _ImageListItem extends _FileListItem {
|
||||||
|
|
||||||
class _VideoListItem extends _FileListItem {
|
class _VideoListItem extends _FileListItem {
|
||||||
_VideoListItem({
|
_VideoListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
DragTargetAccept<DraggableItem> onDropBefore,
|
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||||
DragTargetAccept<DraggableItem> onDropAfter,
|
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||||
VoidCallback onDragStarted,
|
VoidCallback? onDragStarted,
|
||||||
VoidCallback onDragEndedAny,
|
VoidCallback? onDragEndedAny,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
file: file,
|
file: file,
|
||||||
|
@ -806,12 +811,12 @@ class _VideoListItem extends _FileListItem {
|
||||||
|
|
||||||
class _LabelListItem extends _ListItem {
|
class _LabelListItem extends _ListItem {
|
||||||
_LabelListItem({
|
_LabelListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required this.text,
|
required this.text,
|
||||||
DragTargetAccept<DraggableItem> onDropBefore,
|
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||||
DragTargetAccept<DraggableItem> onDropAfter,
|
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||||
VoidCallback onDragStarted,
|
VoidCallback? onDragStarted,
|
||||||
VoidCallback onDragEndedAny,
|
VoidCallback? onDragEndedAny,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
onDropBefore: onDropBefore,
|
onDropBefore: onDropBefore,
|
||||||
|
@ -842,13 +847,13 @@ class _LabelListItem extends _ListItem {
|
||||||
|
|
||||||
class _EditLabelListItem extends _LabelListItem {
|
class _EditLabelListItem extends _LabelListItem {
|
||||||
_EditLabelListItem({
|
_EditLabelListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required String text,
|
required String text,
|
||||||
@required this.onEditPressed,
|
required this.onEditPressed,
|
||||||
DragTargetAccept<DraggableItem> onDropBefore,
|
DragTargetAccept<DraggableItem>? onDropBefore,
|
||||||
DragTargetAccept<DraggableItem> onDropAfter,
|
DragTargetAccept<DraggableItem>? onDropAfter,
|
||||||
VoidCallback onDragStarted,
|
VoidCallback? onDragStarted,
|
||||||
VoidCallback onDragEndedAny,
|
VoidCallback? onDragEndedAny,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
text: text,
|
text: text,
|
||||||
|
@ -873,7 +878,7 @@ class _EditLabelListItem extends _LabelListItem {
|
||||||
end: 0,
|
end: 0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(Icons.edit),
|
icon: Icon(Icons.edit),
|
||||||
tooltip: AppLocalizations.of(context).editTooltip,
|
tooltip: AppLocalizations.of(context)!.editTooltip,
|
||||||
onPressed: onEditPressed,
|
onPressed: onEditPressed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -886,5 +891,5 @@ class _EditLabelListItem extends _LabelListItem {
|
||||||
return super.buildWidget(context);
|
return super.buildWidget(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
final VoidCallback onEditPressed;
|
final VoidCallback? onEditPressed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,20 +18,17 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0);
|
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevelOr(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
File initCover(Account account, List<File> backingFiles) {
|
void initCover(Account account, List<File> backingFiles) {
|
||||||
try {
|
try {
|
||||||
final coverFile =
|
final coverFile =
|
||||||
backingFiles.firstWhere((element) => element.hasPreview);
|
backingFiles.firstWhere((element) => element.hasPreview ?? false);
|
||||||
_coverPreviewUrl = api_util.getFilePreviewUrl(account, coverFile,
|
_coverPreviewUrl = api_util.getFilePreviewUrl(account, coverFile,
|
||||||
width: 1024, height: 600);
|
width: 1024, height: 600);
|
||||||
return coverFile;
|
} catch (_) {}
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
|
@ -39,9 +36,9 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Account account,
|
Account account,
|
||||||
Album album, {
|
Album album, {
|
||||||
List<Widget> actions,
|
List<Widget>? actions,
|
||||||
List<PopupMenuEntry<int>> Function(BuildContext) menuItemBuilder,
|
List<PopupMenuEntry<int>> Function(BuildContext)? menuItemBuilder,
|
||||||
void Function(int) onSelectedMenuItem,
|
void Function(int)? onSelectedMenuItem,
|
||||||
}) {
|
}) {
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
floating: true,
|
floating: true,
|
||||||
|
@ -58,7 +55,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
actions: [
|
actions: [
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Icons.photo_size_select_large),
|
icon: const Icon(Icons.photo_size_select_large),
|
||||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
tooltip: AppLocalizations.of(context)!.zoomTooltip,
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuZoom(
|
PopupMenuZoom(
|
||||||
initialValue: _thumbZoomLevel,
|
initialValue: _thumbZoomLevel,
|
||||||
|
@ -74,12 +71,12 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
...(actions ?? []),
|
...(actions ?? []),
|
||||||
PopupMenuButton(
|
PopupMenuButton<int>(
|
||||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: -1,
|
value: -1,
|
||||||
child: Text(AppLocalizations.of(context).editAlbumMenuLabel),
|
child: Text(AppLocalizations.of(context)!.editAlbumMenuLabel),
|
||||||
),
|
),
|
||||||
...(menuItemBuilder?.call(context) ?? []),
|
...(menuItemBuilder?.call(context) ?? []),
|
||||||
],
|
],
|
||||||
|
@ -120,7 +117,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)!
|
||||||
.selectionAppBarTitle(selectedListItems.length)),
|
.selectionAppBarTitle(selectedListItems.length)),
|
||||||
actions: actions,
|
actions: actions,
|
||||||
),
|
),
|
||||||
|
@ -132,7 +129,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Account account,
|
Account account,
|
||||||
Album album, {
|
Album album, {
|
||||||
List<Widget> actions,
|
List<Widget>? actions,
|
||||||
}) {
|
}) {
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
floating: true,
|
floating: true,
|
||||||
|
@ -141,16 +138,17 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
background: _getAppBarCover(context, account),
|
background: _getAppBarCover(context, account),
|
||||||
title: TextFormField(
|
title: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(context).nameInputHint,
|
hintText: AppLocalizations.of(context)!.nameInputHint,
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value.isEmpty) {
|
if (value?.isNotEmpty == true) {
|
||||||
return AppLocalizations.of(context).albumNameInputInvalidEmpty;
|
return null;
|
||||||
|
} else {
|
||||||
|
return AppLocalizations.of(context)!.albumNameInputInvalidEmpty;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_editFormValue.name = value;
|
_editFormValue.name = value!;
|
||||||
},
|
},
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
// need to save the value otherwise it'll return to the initial
|
// need to save the value otherwise it'll return to the initial
|
||||||
|
@ -166,7 +164,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
tooltip: AppLocalizations.of(context).doneButtonTooltip,
|
tooltip: AppLocalizations.of(context)!.doneButtonTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (validateEditMode()) {
|
if (validateEditMode()) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -226,7 +224,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _getAppBarCover(BuildContext context, Account account) {
|
Widget? _getAppBarCover(BuildContext context, Account account) {
|
||||||
try {
|
try {
|
||||||
if (_coverPreviewUrl != null) {
|
if (_coverPreviewUrl != null) {
|
||||||
return Opacity(
|
return Opacity(
|
||||||
|
@ -236,7 +234,7 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: _coverPreviewUrl,
|
imageUrl: _coverPreviewUrl!,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
"Authorization": Api.getAuthorizationHeaderValue(account),
|
"Authorization": Api.getAuthorizationHeaderValue(account),
|
||||||
},
|
},
|
||||||
|
@ -254,11 +252,11 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _coverPreviewUrl;
|
String? _coverPreviewUrl;
|
||||||
var _thumbZoomLevel = 0;
|
var _thumbZoomLevel = 0;
|
||||||
|
|
||||||
var _isEditMode = false;
|
var _isEditMode = false;
|
||||||
String _editNameValue;
|
String? _editNameValue;
|
||||||
var _editFormValue = _EditFormValue();
|
var _editFormValue = _EditFormValue();
|
||||||
|
|
||||||
static final _log = Logger("widget.album_viewer_mixin.AlbumViewerMixin");
|
static final _log = Logger("widget.album_viewer_mixin.AlbumViewerMixin");
|
||||||
|
@ -266,5 +264,5 @@ mixin AlbumViewerMixin<T extends StatefulWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EditFormValue {
|
class _EditFormValue {
|
||||||
String name;
|
late String name;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,14 @@ import 'package:flutter/widgets.dart';
|
||||||
/// The point is to disable non-visible buttons
|
/// The point is to disable non-visible buttons
|
||||||
class AnimatedVisibility extends StatefulWidget {
|
class AnimatedVisibility extends StatefulWidget {
|
||||||
const AnimatedVisibility({
|
const AnimatedVisibility({
|
||||||
Key key,
|
Key? key,
|
||||||
this.child,
|
required this.child,
|
||||||
@required this.opacity,
|
required this.opacity,
|
||||||
this.curve = Curves.linear,
|
this.curve = Curves.linear,
|
||||||
@required this.duration,
|
required this.duration,
|
||||||
this.onEnd,
|
this.onEnd,
|
||||||
this.alwaysIncludeSemantics = false,
|
this.alwaysIncludeSemantics = false,
|
||||||
}) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
|
}) : assert(opacity >= 0.0 && opacity <= 1.0),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -22,7 +22,7 @@ class AnimatedVisibility extends StatefulWidget {
|
||||||
final double opacity;
|
final double opacity;
|
||||||
final Curve curve;
|
final Curve curve;
|
||||||
final Duration duration;
|
final Duration duration;
|
||||||
final VoidCallback onEnd;
|
final VoidCallback? onEnd;
|
||||||
final bool alwaysIncludeSemantics;
|
final bool alwaysIncludeSemantics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,12 +32,16 @@ class ArchiveViewerArguments {
|
||||||
class ArchiveViewer extends StatefulWidget {
|
class ArchiveViewer extends StatefulWidget {
|
||||||
static const routeName = "/archive-viewer";
|
static const routeName = "/archive-viewer";
|
||||||
|
|
||||||
|
static Route buildRoute(ArchiveViewerArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => ArchiveViewer.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
ArchiveViewer({
|
ArchiveViewer({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
ArchiveViewer.fromArgs(ArchiveViewerArguments args, {Key key})
|
ArchiveViewer.fromArgs(ArchiveViewerArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -55,7 +59,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initBloc();
|
_initBloc();
|
||||||
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevel(0);
|
_thumbZoomLevel = Pref.inst().getAlbumViewerZoomLevelOr(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -81,7 +85,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
_reqQuery();
|
_reqQuery();
|
||||||
} else {
|
} else {
|
||||||
// process the current state
|
// process the current state
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onStateChange(context, _bloc.state);
|
_onStateChange(context, _bloc.state);
|
||||||
});
|
});
|
||||||
|
@ -144,12 +148,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)!
|
||||||
.selectionAppBarTitle(selectedListItems.length)),
|
.selectionAppBarTitle(selectedListItems.length)),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.unarchive),
|
icon: const Icon(Icons.unarchive),
|
||||||
tooltip: AppLocalizations.of(context).unarchiveSelectedTooltip,
|
tooltip: AppLocalizations.of(context)!.unarchiveSelectedTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_onSelectionAppBarUnarchivePressed();
|
_onSelectionAppBarUnarchivePressed();
|
||||||
},
|
},
|
||||||
|
@ -161,12 +165,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
|
|
||||||
Widget _buildNormalAppBar(BuildContext context) {
|
Widget _buildNormalAppBar(BuildContext context) {
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
title: Text(AppLocalizations.of(context).albumArchiveLabel),
|
title: Text(AppLocalizations.of(context)!.albumArchiveLabel),
|
||||||
floating: true,
|
floating: true,
|
||||||
actions: [
|
actions: [
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Icons.photo_size_select_large),
|
icon: const Icon(Icons.photo_size_select_large),
|
||||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
tooltip: AppLocalizations.of(context)!.zoomTooltip,
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuZoom(
|
PopupMenuZoom(
|
||||||
initialValue: _thumbZoomLevel,
|
initialValue: _thumbZoomLevel,
|
||||||
|
@ -208,7 +212,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
|
|
||||||
Future<void> _onSelectionAppBarUnarchivePressed() async {
|
Future<void> _onSelectionAppBarUnarchivePressed() async {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.unarchiveSelectedProcessingNotification(selectedListItems.length)),
|
.unarchiveSelectedProcessingNotification(selectedListItems.length)),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
|
@ -237,12 +241,12 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
if (failures.isEmpty) {
|
if (failures.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).unarchiveSelectedSuccessNotification),
|
AppLocalizations.of(context)!.unarchiveSelectedSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.unarchiveSelectedFailureNotification(failures.length)),
|
.unarchiveSelectedFailureNotification(failures.length)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -308,7 +312,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanDirBloc _bloc;
|
late ScanDirBloc _bloc;
|
||||||
|
|
||||||
var _backingFiles = <File>[];
|
var _backingFiles = <File>[];
|
||||||
|
|
||||||
|
@ -319,7 +323,7 @@ class _ArchiveViewerState extends State<ArchiveViewer>
|
||||||
|
|
||||||
abstract class _ListItem implements SelectableItem {
|
abstract class _ListItem implements SelectableItem {
|
||||||
_ListItem({
|
_ListItem({
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : _onTap = onTap;
|
}) : _onTap = onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -331,13 +335,13 @@ abstract class _ListItem implements SelectableItem {
|
||||||
@override
|
@override
|
||||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||||
|
|
||||||
final VoidCallback _onTap;
|
final VoidCallback? _onTap;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _FileListItem extends _ListItem {
|
abstract class _FileListItem extends _ListItem {
|
||||||
_FileListItem({
|
_FileListItem({
|
||||||
@required this.file,
|
required this.file,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(onTap: onTap);
|
}) : super(onTap: onTap);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -353,10 +357,10 @@ abstract class _FileListItem extends _ListItem {
|
||||||
|
|
||||||
class _ImageListItem extends _FileListItem {
|
class _ImageListItem extends _FileListItem {
|
||||||
_ImageListItem({
|
_ImageListItem({
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(file: file, onTap: onTap);
|
}) : super(file: file, onTap: onTap);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -374,10 +378,10 @@ class _ImageListItem extends _FileListItem {
|
||||||
|
|
||||||
class _VideoListItem extends _FileListItem {
|
class _VideoListItem extends _FileListItem {
|
||||||
_VideoListItem({
|
_VideoListItem({
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(file: file, onTap: onTap);
|
}) : super(file: file, onTap: onTap);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -13,8 +13,8 @@ import 'package:nc_photos/widget/album_grid_item.dart';
|
||||||
/// Build a standard [AlbumGridItem] for an [Album]
|
/// Build a standard [AlbumGridItem] for an [Album]
|
||||||
class AlbumGridItemBuilder {
|
class AlbumGridItemBuilder {
|
||||||
AlbumGridItemBuilder({
|
AlbumGridItemBuilder({
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.album,
|
required this.album,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
|
@ -22,9 +22,9 @@ class AlbumGridItemBuilder {
|
||||||
|
|
||||||
AlbumGridItem build(BuildContext context) {
|
AlbumGridItem build(BuildContext context) {
|
||||||
var subtitle = "";
|
var subtitle = "";
|
||||||
String subtitle2;
|
String? subtitle2;
|
||||||
if (album.provider is AlbumStaticProvider) {
|
if (album.provider is AlbumStaticProvider) {
|
||||||
subtitle = AppLocalizations.of(context)
|
subtitle = AppLocalizations.of(context)!
|
||||||
.albumSize(AlbumStaticProvider.of(album).items.length);
|
.albumSize(AlbumStaticProvider.of(album).items.length);
|
||||||
} else if (album.provider is AlbumDirProvider) {
|
} else if (album.provider is AlbumDirProvider) {
|
||||||
final provider = album.provider as AlbumDirProvider;
|
final provider = album.provider as AlbumDirProvider;
|
||||||
|
@ -49,7 +49,7 @@ class AlbumGridItemBuilder {
|
||||||
Widget cover;
|
Widget cover;
|
||||||
try {
|
try {
|
||||||
final coverFile = album.coverProvider.getCover(album);
|
final coverFile = album.coverProvider.getCover(album);
|
||||||
final previewUrl = api_util.getFilePreviewUrl(account, coverFile,
|
final previewUrl = api_util.getFilePreviewUrl(account, coverFile!,
|
||||||
width: 512, height: 512);
|
width: 512, height: 512);
|
||||||
cover = FittedBox(
|
cover = FittedBox(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
|
@ -89,6 +89,6 @@ class AlbumGridItemBuilder {
|
||||||
final Account account;
|
final Account account;
|
||||||
final Album album;
|
final Album album;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
/// to clear the image from the [ImageCache].
|
/// to clear the image from the [ImageCache].
|
||||||
static Future evictFromCache(
|
static Future evictFromCache(
|
||||||
String url, {
|
String url, {
|
||||||
String cacheKey,
|
String? cacheKey,
|
||||||
BaseCacheManager cacheManager,
|
BaseCacheManager? cacheManager,
|
||||||
double scale = 1.0,
|
double scale = 1.0,
|
||||||
}) async {
|
}) async {
|
||||||
cacheManager = cacheManager ?? DefaultCacheManager();
|
cacheManager = cacheManager ?? DefaultCacheManager();
|
||||||
|
@ -33,31 +33,31 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
final CachedNetworkImageProvider _image;
|
final CachedNetworkImageProvider _image;
|
||||||
|
|
||||||
/// Option to use cachemanager with other settings
|
/// Option to use cachemanager with other settings
|
||||||
final BaseCacheManager cacheManager;
|
final BaseCacheManager? cacheManager;
|
||||||
|
|
||||||
/// The target image that is displayed.
|
/// The target image that is displayed.
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
|
|
||||||
/// The target image's cache key.
|
/// The target image's cache key.
|
||||||
final String cacheKey;
|
final String? cacheKey;
|
||||||
|
|
||||||
/// Optional builder to further customize the display of the image.
|
/// Optional builder to further customize the display of the image.
|
||||||
final ImageWidgetBuilder imageBuilder;
|
final ImageWidgetBuilder? imageBuilder;
|
||||||
|
|
||||||
/// Widget displayed while the target [imageUrl] is loading.
|
/// Widget displayed while the target [imageUrl] is loading.
|
||||||
final PlaceholderWidgetBuilder placeholder;
|
final PlaceholderWidgetBuilder? placeholder;
|
||||||
|
|
||||||
/// Widget displayed while the target [imageUrl] is loading.
|
/// Widget displayed while the target [imageUrl] is loading.
|
||||||
final ProgressIndicatorBuilder progressIndicatorBuilder;
|
final ProgressIndicatorBuilder? progressIndicatorBuilder;
|
||||||
|
|
||||||
/// Widget displayed while the target [imageUrl] failed loading.
|
/// Widget displayed while the target [imageUrl] failed loading.
|
||||||
final LoadingErrorWidgetBuilder errorWidget;
|
final LoadingErrorWidgetBuilder? errorWidget;
|
||||||
|
|
||||||
/// The duration of the fade-in animation for the [placeholder].
|
/// The duration of the fade-in animation for the [placeholder].
|
||||||
final Duration placeholderFadeInDuration;
|
final Duration? placeholderFadeInDuration;
|
||||||
|
|
||||||
/// The duration of the fade-out animation for the [placeholder].
|
/// The duration of the fade-out animation for the [placeholder].
|
||||||
final Duration fadeOutDuration;
|
final Duration? fadeOutDuration;
|
||||||
|
|
||||||
/// The curve of the fade-out animation for the [placeholder].
|
/// The curve of the fade-out animation for the [placeholder].
|
||||||
final Curve fadeOutCurve;
|
final Curve fadeOutCurve;
|
||||||
|
@ -74,7 +74,7 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
/// aspect ratio. This may result in a sudden change if the size of the
|
/// aspect ratio. This may result in a sudden change if the size of the
|
||||||
/// placeholder widget does not match that of the target image. The size is
|
/// placeholder widget does not match that of the target image. The size is
|
||||||
/// also affected by the scale factor.
|
/// also affected by the scale factor.
|
||||||
final double width;
|
final double? width;
|
||||||
|
|
||||||
/// If non-null, require the image to have this height.
|
/// If non-null, require the image to have this height.
|
||||||
///
|
///
|
||||||
|
@ -82,13 +82,13 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
/// aspect ratio. This may result in a sudden change if the size of the
|
/// aspect ratio. This may result in a sudden change if the size of the
|
||||||
/// placeholder widget does not match that of the target image. The size is
|
/// placeholder widget does not match that of the target image. The size is
|
||||||
/// also affected by the scale factor.
|
/// also affected by the scale factor.
|
||||||
final double height;
|
final double? height;
|
||||||
|
|
||||||
/// How to inscribe the image into the space allocated during layout.
|
/// How to inscribe the image into the space allocated during layout.
|
||||||
///
|
///
|
||||||
/// The default varies based on the other fields. See the discussion at
|
/// The default varies based on the other fields. See the discussion at
|
||||||
/// [paintImage].
|
/// [paintImage].
|
||||||
final BoxFit fit;
|
final BoxFit? fit;
|
||||||
|
|
||||||
/// How to align the image within its bounds.
|
/// How to align the image within its bounds.
|
||||||
///
|
///
|
||||||
|
@ -112,7 +112,7 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
/// specify an [AlignmentGeometry].
|
/// specify an [AlignmentGeometry].
|
||||||
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
|
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
|
||||||
/// relative to text direction.
|
/// relative to text direction.
|
||||||
final AlignmentGeometry alignment;
|
final Alignment alignment;
|
||||||
|
|
||||||
/// How to paint any portions of the layout bounds not covered by the image.
|
/// How to paint any portions of the layout bounds not covered by the image.
|
||||||
final ImageRepeat repeat;
|
final ImageRepeat repeat;
|
||||||
|
@ -135,14 +135,14 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
final bool matchTextDirection;
|
final bool matchTextDirection;
|
||||||
|
|
||||||
/// Optional headers for the http request of the image url
|
/// Optional headers for the http request of the image url
|
||||||
final Map<String, String> httpHeaders;
|
final Map<String, String>? httpHeaders;
|
||||||
|
|
||||||
/// When set to true it will animate from the old image to the new image
|
/// When set to true it will animate from the old image to the new image
|
||||||
/// if the url changes.
|
/// if the url changes.
|
||||||
final bool useOldImageOnUrlChange;
|
final bool useOldImageOnUrlChange;
|
||||||
|
|
||||||
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
|
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
|
||||||
final Color color;
|
final Color? color;
|
||||||
|
|
||||||
/// Used to combine [color] with this image.
|
/// Used to combine [color] with this image.
|
||||||
///
|
///
|
||||||
|
@ -152,7 +152,7 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [BlendMode], which includes an illustration of the effect of each blend mode.
|
/// * [BlendMode], which includes an illustration of the effect of each blend mode.
|
||||||
final BlendMode colorBlendMode;
|
final BlendMode? colorBlendMode;
|
||||||
|
|
||||||
/// Target the interpolation quality for image scaling.
|
/// Target the interpolation quality for image scaling.
|
||||||
///
|
///
|
||||||
|
@ -160,24 +160,24 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
final FilterQuality filterQuality;
|
final FilterQuality filterQuality;
|
||||||
|
|
||||||
/// Will resize the image in memory to have a certain width using [ResizeImage]
|
/// Will resize the image in memory to have a certain width using [ResizeImage]
|
||||||
final int memCacheWidth;
|
final int? memCacheWidth;
|
||||||
|
|
||||||
/// Will resize the image in memory to have a certain height using [ResizeImage]
|
/// Will resize the image in memory to have a certain height using [ResizeImage]
|
||||||
final int memCacheHeight;
|
final int? memCacheHeight;
|
||||||
|
|
||||||
/// Will resize the image and store the resized image in the disk cache.
|
/// Will resize the image and store the resized image in the disk cache.
|
||||||
final int maxWidthDiskCache;
|
final int? maxWidthDiskCache;
|
||||||
|
|
||||||
/// Will resize the image and store the resized image in the disk cache.
|
/// Will resize the image and store the resized image in the disk cache.
|
||||||
final int maxHeightDiskCache;
|
final int? maxHeightDiskCache;
|
||||||
|
|
||||||
/// CachedNetworkImage shows a network image using a caching mechanism. It also
|
/// CachedNetworkImage shows a network image using a caching mechanism. It also
|
||||||
/// provides support for a placeholder, showing an error and fading into the
|
/// provides support for a placeholder, showing an error and fading into the
|
||||||
/// loaded image. Next to that it supports most features of a default Image
|
/// loaded image. Next to that it supports most features of a default Image
|
||||||
/// widget.
|
/// widget.
|
||||||
CachedNetworkImage({
|
CachedNetworkImage({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.imageUrl,
|
required this.imageUrl,
|
||||||
this.httpHeaders,
|
this.httpHeaders,
|
||||||
this.imageBuilder,
|
this.imageBuilder,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
|
@ -204,17 +204,8 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
this.cacheKey,
|
this.cacheKey,
|
||||||
this.maxWidthDiskCache,
|
this.maxWidthDiskCache,
|
||||||
this.maxHeightDiskCache,
|
this.maxHeightDiskCache,
|
||||||
ImageRenderMethodForWeb imageRenderMethodForWeb,
|
ImageRenderMethodForWeb? imageRenderMethodForWeb,
|
||||||
}) : assert(imageUrl != null),
|
}) : _image = CachedNetworkImageProvider(
|
||||||
assert(fadeOutDuration != null),
|
|
||||||
assert(fadeOutCurve != null),
|
|
||||||
assert(fadeInDuration != null),
|
|
||||||
assert(fadeInCurve != null),
|
|
||||||
assert(alignment != null),
|
|
||||||
assert(filterQuality != null),
|
|
||||||
assert(repeat != null),
|
|
||||||
assert(matchTextDirection != null),
|
|
||||||
_image = CachedNetworkImageProvider(
|
|
||||||
imageUrl,
|
imageUrl,
|
||||||
headers: httpHeaders,
|
headers: httpHeaders,
|
||||||
cacheManager: cacheManager,
|
cacheManager: cacheManager,
|
||||||
|
@ -267,32 +258,32 @@ class CachedNetworkImage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _octoImageBuilder(BuildContext context, Widget child) {
|
Widget _octoImageBuilder(BuildContext context, Widget child) {
|
||||||
return imageBuilder(context, child, _image);
|
return imageBuilder!(context, child, _image);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _octoPlaceholderBuilder(BuildContext context) {
|
Widget _octoPlaceholderBuilder(BuildContext context) {
|
||||||
return placeholder(context, imageUrl);
|
return placeholder!(context, imageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _octoProgressIndicatorBuilder(
|
Widget _octoProgressIndicatorBuilder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ImageChunkEvent progress,
|
ImageChunkEvent? progress,
|
||||||
) {
|
) {
|
||||||
int totalSize;
|
int? totalSize;
|
||||||
var downloaded = 0;
|
var downloaded = 0;
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
totalSize = progress.expectedTotalBytes;
|
totalSize = progress.expectedTotalBytes;
|
||||||
downloaded = progress.cumulativeBytesLoaded;
|
downloaded = progress.cumulativeBytesLoaded;
|
||||||
}
|
}
|
||||||
return progressIndicatorBuilder(
|
return progressIndicatorBuilder!(
|
||||||
context, imageUrl, DownloadProgress(imageUrl, totalSize, downloaded));
|
context, imageUrl, DownloadProgress(imageUrl, totalSize, downloaded));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _octoErrorBuilder(
|
Widget _octoErrorBuilder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Object error,
|
Object error,
|
||||||
StackTrace stackTrace,
|
StackTrace? stackTrace,
|
||||||
) {
|
) {
|
||||||
return errorWidget(context, imageUrl, error);
|
return errorWidget!(context, imageUrl, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,16 @@ class ConnectArguments {
|
||||||
class Connect extends StatefulWidget {
|
class Connect extends StatefulWidget {
|
||||||
static const routeName = "/connect";
|
static const routeName = "/connect";
|
||||||
|
|
||||||
|
static Route buildRoute(ConnectArguments args) => MaterialPageRoute<Account>(
|
||||||
|
builder: (context) => Connect.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
Connect({
|
Connect({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Connect.fromArgs(ConnectArguments args, {Key key})
|
Connect.fromArgs(ConnectArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -82,7 +86,7 @@ class _ConnectState extends State<Connect> {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)
|
AppLocalizations.of(context)!
|
||||||
.connectingToServer(widget.account.url),
|
.connectingToServer(widget.account.url),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
@ -106,7 +110,7 @@ class _ConnectState extends State<Connect> {
|
||||||
} else if (state.exception is ApiException &&
|
} else if (state.exception is ApiException &&
|
||||||
(state.exception as ApiException).response.statusCode == 401) {
|
(state.exception as ApiException).response.statusCode == 401) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).errorWrongPassword),
|
content: Text(AppLocalizations.of(context)!.errorWrongPassword),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
Navigator.of(context).pop(null);
|
Navigator.of(context).pop(null);
|
||||||
|
@ -124,9 +128,9 @@ class _ConnectState extends State<Connect> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context).serverCertErrorDialogTitle),
|
title: Text(AppLocalizations.of(context)!.serverCertErrorDialogTitle),
|
||||||
content:
|
content:
|
||||||
Text(AppLocalizations.of(context).serverCertErrorDialogContent),
|
Text(AppLocalizations.of(context)!.serverCertErrorDialogContent),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -138,7 +142,7 @@ class _ConnectState extends State<Connect> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(true);
|
Navigator.of(context).pop(true);
|
||||||
},
|
},
|
||||||
child: Text(AppLocalizations.of(context).advancedButtonLabel),
|
child: Text(AppLocalizations.of(context)!.advancedButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -150,10 +154,11 @@ class _ConnectState extends State<Connect> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context).whitelistCertDialogTitle),
|
title: Text(AppLocalizations.of(context)!.whitelistCertDialogTitle),
|
||||||
content: Text(AppLocalizations.of(context).whitelistCertDialogContent(
|
content: Text(AppLocalizations.of(context)!
|
||||||
SelfSignedCertManager().getLastBadCertHost(),
|
.whitelistCertDialogContent(
|
||||||
SelfSignedCertManager().getLastBadCertFingerprint())),
|
SelfSignedCertManager().getLastBadCertHost(),
|
||||||
|
SelfSignedCertManager().getLastBadCertFingerprint())),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -166,7 +171,7 @@ class _ConnectState extends State<Connect> {
|
||||||
Navigator.of(context).pop(true);
|
Navigator.of(context).pop(true);
|
||||||
},
|
},
|
||||||
child:
|
child:
|
||||||
Text(AppLocalizations.of(context).whitelistCertButtonLabel),
|
Text(AppLocalizations.of(context)!.whitelistCertButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -100,7 +100,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const SizedBox(width: 24),
|
leading: const SizedBox(width: 24),
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).rootPickerNavigateUpItemText),
|
AppLocalizations.of(context)!.rootPickerNavigateUpItemText),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
try {
|
try {
|
||||||
_navigateInto(File(path: path.dirname(_currentPath)));
|
_navigateInto(File(path: path.dirname(_currentPath)));
|
||||||
|
@ -128,7 +128,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
final canPick = canPickDir(item.file);
|
final canPick = canPickDir(item.file);
|
||||||
final pickState = _isItemPicked(item);
|
final pickState = _isItemPicked(item);
|
||||||
|
|
||||||
IconData iconData;
|
IconData? iconData;
|
||||||
if (canPick) {
|
if (canPick) {
|
||||||
switch (pickState) {
|
switch (pickState) {
|
||||||
case _PickState.picked:
|
case _PickState.picked:
|
||||||
|
@ -170,9 +170,10 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
),
|
),
|
||||||
title: Text(path.basename(item.file.path)),
|
title: Text(path.basename(item.file.path)),
|
||||||
trailing:
|
trailing: item.children?.isNotEmpty == true
|
||||||
item.children.isNotEmpty ? const Icon(Icons.arrow_forward_ios) : null,
|
? const Icon(Icons.arrow_forward_ios)
|
||||||
onTap: item.children.isNotEmpty
|
: null,
|
||||||
|
onTap: item.children?.isNotEmpty == true
|
||||||
? () {
|
? () {
|
||||||
try {
|
try {
|
||||||
_navigateInto(item.file);
|
_navigateInto(item.file);
|
||||||
|
@ -238,12 +239,12 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
// this dir is explicitly picked, nothing more to do
|
// this dir is explicitly picked, nothing more to do
|
||||||
return [item.file];
|
return [item.file];
|
||||||
}
|
}
|
||||||
if (item.children == null || item.children.isEmpty) {
|
if (item.children == null || item.children!.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
final products = <File>[];
|
final products = <File>[];
|
||||||
for (final i in item.children) {
|
for (final i in item.children!) {
|
||||||
products.addAll(_optimizePicks(i));
|
products.addAll(_optimizePicks(i));
|
||||||
}
|
}
|
||||||
// // see if all children are being picked
|
// // see if all children are being picked
|
||||||
|
@ -282,7 +283,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
_picks.removeWhere((element) => identical(element, parent));
|
_picks.removeWhere((element) => identical(element, parent));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.rootPickerUnpickFailureNotification)));
|
.rootPickerUnpickFailureNotification)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,12 +297,13 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
/// Either [path] or [item] must be set, If both are set, [item] takes
|
/// Either [path] or [item] must be set, If both are set, [item] takes
|
||||||
/// priority
|
/// priority
|
||||||
List<LsDirBlocItem> _pickedAllExclude({
|
List<LsDirBlocItem> _pickedAllExclude({
|
||||||
String path,
|
String? path,
|
||||||
LsDirBlocItem item,
|
LsDirBlocItem? item,
|
||||||
@required LsDirBlocItem exclude,
|
required LsDirBlocItem exclude,
|
||||||
}) {
|
}) {
|
||||||
|
assert(path != null || item != null);
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
final item = _findChildItemByPath(_root, path);
|
final item = _findChildItemByPath(_root, path!);
|
||||||
return _pickedAllExclude(item: item, exclude: exclude);
|
return _pickedAllExclude(item: item, exclude: exclude);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +313,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
_log.fine(
|
_log.fine(
|
||||||
"[_pickedAllExclude] Unpicking '${item.file.path}' and picking children");
|
"[_pickedAllExclude] Unpicking '${item.file.path}' and picking children");
|
||||||
final products = <LsDirBlocItem>[];
|
final products = <LsDirBlocItem>[];
|
||||||
for (final i in item.children) {
|
for (final i in item.children ?? []) {
|
||||||
if (exclude.file.path.startsWith(i.file.path)) {
|
if (exclude.file.path.startsWith(i.file.path)) {
|
||||||
// [i] is a parent of exclude
|
// [i] is a parent of exclude
|
||||||
products.addAll(_pickedAllExclude(item: i, exclude: exclude));
|
products.addAll(_pickedAllExclude(item: i, exclude: exclude));
|
||||||
|
@ -327,7 +329,7 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
if (path == parent.file.path) {
|
if (path == parent.file.path) {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
for (final c in parent.children) {
|
for (final c in parent.children ?? []) {
|
||||||
if (path == c.file.path || path.startsWith("${c.file.path}/")) {
|
if (path == c.file.path || path.startsWith("${c.file.path}/")) {
|
||||||
return _findChildItemByPath(c, path);
|
return _findChildItemByPath(c, path);
|
||||||
}
|
}
|
||||||
|
@ -364,11 +366,11 @@ mixin DirPickerMixin<T extends StatefulWidget> on State<T> {
|
||||||
_bloc.add(LsDirBlocQuery(getAccount(), file, depth: 2));
|
_bloc.add(LsDirBlocQuery(getAccount(), file, depth: 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
LsDirBloc _bloc;
|
late LsDirBloc _bloc;
|
||||||
LsDirBlocItem _root;
|
late LsDirBlocItem _root;
|
||||||
|
|
||||||
/// Track where the user is navigating in [_backingFiles]
|
/// Track where the user is navigating in [_backingFiles]
|
||||||
String _currentPath;
|
late String _currentPath;
|
||||||
var _picks = <File>[];
|
var _picks = <File>[];
|
||||||
|
|
||||||
static final _log = Logger("widget.dir_picker_mixin.DirPickerMixin");
|
static final _log = Logger("widget.dir_picker_mixin.DirPickerMixin");
|
||||||
|
|
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
class Draggable<T> extends StatelessWidget {
|
class Draggable<T extends Object> extends StatelessWidget {
|
||||||
Draggable({
|
Draggable({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.data,
|
required this.data,
|
||||||
@required this.child,
|
required this.child,
|
||||||
this.feedback,
|
this.feedback,
|
||||||
this.onDropBefore,
|
this.onDropBefore,
|
||||||
this.onDropAfter,
|
this.onDropAfter,
|
||||||
|
@ -79,7 +79,7 @@ class Draggable<T> extends StatelessWidget {
|
||||||
},
|
},
|
||||||
onAccept: (item) {
|
onAccept: (item) {
|
||||||
_log.fine("[build] Dropping $item before $data");
|
_log.fine("[build] Dropping $item before $data");
|
||||||
onDropBefore(item);
|
onDropBefore!(item);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -92,7 +92,7 @@ class Draggable<T> extends StatelessWidget {
|
||||||
},
|
},
|
||||||
onAccept: (item) {
|
onAccept: (item) {
|
||||||
_log.fine("[build] Dropping $item after $data");
|
_log.fine("[build] Dropping $item after $data");
|
||||||
onDropAfter(item);
|
onDropAfter!(item);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -105,26 +105,26 @@ class Draggable<T> extends StatelessWidget {
|
||||||
|
|
||||||
final T data;
|
final T data;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Widget feedback;
|
final Widget? feedback;
|
||||||
|
|
||||||
/// Called when some item dropped before this item
|
/// Called when some item dropped before this item
|
||||||
final DragTargetAccept<T> onDropBefore;
|
final DragTargetAccept<T>? onDropBefore;
|
||||||
|
|
||||||
/// Called when some item dropped after this item
|
/// Called when some item dropped after this item
|
||||||
final DragTargetAccept<T> onDropAfter;
|
final DragTargetAccept<T>? onDropAfter;
|
||||||
|
|
||||||
final VoidCallback onDragStarted;
|
final VoidCallback? onDragStarted;
|
||||||
|
|
||||||
/// Called when either one of onDragEnd, onDragCompleted or
|
/// Called when either one of onDragEnd, onDragCompleted or
|
||||||
/// onDraggableCanceled is called.
|
/// onDraggableCanceled is called.
|
||||||
///
|
///
|
||||||
/// The callback might be called multiple times per each drag event
|
/// The callback might be called multiple times per each drag event
|
||||||
final VoidCallback onDragEndedAny;
|
final VoidCallback? onDragEndedAny;
|
||||||
|
|
||||||
/// Size of the feedback widget that appears under the pointer.
|
/// Size of the feedback widget that appears under the pointer.
|
||||||
///
|
///
|
||||||
/// Right now a translucent version of [child] is being shown
|
/// Right now a translucent version of [child] is being shown
|
||||||
final Size feedbackSize;
|
final Size? feedbackSize;
|
||||||
|
|
||||||
static final _log = Logger("widget.draggable.Draggable");
|
static final _log = Logger("widget.draggable.Draggable");
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,21 +9,21 @@ abstract class DraggableItem {
|
||||||
/// The widget to show under the pointer when a drag is under way.
|
/// The widget to show under the pointer when a drag is under way.
|
||||||
///
|
///
|
||||||
/// Return null if you wish to just use the same widget as display
|
/// Return null if you wish to just use the same widget as display
|
||||||
Widget buildDragFeedbackWidget(BuildContext context) => null;
|
Widget? buildDragFeedbackWidget(BuildContext context) => null;
|
||||||
|
|
||||||
bool get isDraggable => false;
|
bool get isDraggable => false;
|
||||||
DragTargetAccept<DraggableItem> get onDropBefore => null;
|
DragTargetAccept<DraggableItem>? get onDropBefore => null;
|
||||||
DragTargetAccept<DraggableItem> get onDropAfter => null;
|
DragTargetAccept<DraggableItem>? get onDropAfter => null;
|
||||||
VoidCallback get onDragStarted => null;
|
VoidCallback? get onDragStarted => null;
|
||||||
VoidCallback get onDragEndedAny => null;
|
VoidCallback? get onDragEndedAny => null;
|
||||||
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
|
mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
|
||||||
@protected
|
@protected
|
||||||
Widget buildDraggableItemList({
|
Widget buildDraggableItemList({
|
||||||
@required double maxCrossAxisExtent,
|
required double maxCrossAxisExtent,
|
||||||
ValueChanged<double> onMaxExtentChanged,
|
ValueChanged<double?>? onMaxExtentChanged,
|
||||||
}) {
|
}) {
|
||||||
_maxCrossAxisExtent = maxCrossAxisExtent;
|
_maxCrossAxisExtent = maxCrossAxisExtent;
|
||||||
return MeasurableItemList(
|
return MeasurableItemList(
|
||||||
|
@ -50,12 +50,10 @@ mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
|
||||||
onDropAfter: item.onDropAfter,
|
onDropAfter: item.onDropAfter,
|
||||||
onDragStarted: item.onDragStarted,
|
onDragStarted: item.onDragStarted,
|
||||||
onDragEndedAny: item.onDragEndedAny,
|
onDragEndedAny: item.onDragEndedAny,
|
||||||
feedbackSize: _maxCrossAxisExtent != null
|
feedbackSize: Size(_maxCrossAxisExtent * .65, _maxCrossAxisExtent * .65),
|
||||||
? Size(_maxCrossAxisExtent * .65, _maxCrossAxisExtent * .65)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var _items = <DraggableItem>[];
|
var _items = <DraggableItem>[];
|
||||||
double _maxCrossAxisExtent;
|
late double _maxCrossAxisExtent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,13 +43,18 @@ class DynamicAlbumViewerArguments {
|
||||||
class DynamicAlbumViewer extends StatefulWidget {
|
class DynamicAlbumViewer extends StatefulWidget {
|
||||||
static const routeName = "/dynamic-album-viewer";
|
static const routeName = "/dynamic-album-viewer";
|
||||||
|
|
||||||
|
static Route buildRoute(DynamicAlbumViewerArguments args) =>
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => DynamicAlbumViewer.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
DynamicAlbumViewer({
|
DynamicAlbumViewer({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.album,
|
required this.album,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
DynamicAlbumViewer.fromArgs(DynamicAlbumViewerArguments args, {Key key})
|
DynamicAlbumViewer.fromArgs(DynamicAlbumViewerArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -96,19 +101,19 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
@override
|
@override
|
||||||
enterEditMode() {
|
enterEditMode() {
|
||||||
super.enterEditMode();
|
super.enterEditMode();
|
||||||
_editAlbum = _album.copyWith();
|
_editAlbum = _album!.copyWith();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
validateEditMode() => _editFormKey?.currentState?.validate() == true;
|
validateEditMode() => _editFormKey.currentState?.validate() == true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
doneEditMode() {
|
doneEditMode() {
|
||||||
try {
|
try {
|
||||||
// persist the changes
|
// persist the changes
|
||||||
_editFormKey.currentState.save();
|
_editFormKey.currentState!.save();
|
||||||
final newAlbum = makeEdited(_editAlbum);
|
final newAlbum = makeEdited(_editAlbum!);
|
||||||
if (newAlbum.copyWith(lastUpdated: OrNull(_album.lastUpdated)) !=
|
if (newAlbum.copyWith(lastUpdated: OrNull(_album!.lastUpdated)) !=
|
||||||
_album) {
|
_album) {
|
||||||
_log.info("[doneEditMode] Album modified: $newAlbum");
|
_log.info("[doneEditMode] Album modified: $newAlbum");
|
||||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||||
|
@ -142,14 +147,14 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
setState(() {
|
setState(() {
|
||||||
_album = widget.album;
|
_album = widget.album;
|
||||||
_transformItems(items);
|
_transformItems(items);
|
||||||
final coverFile = initCover(widget.account, _backingFiles);
|
initCover(widget.account, _backingFiles);
|
||||||
_updateAlbumPostPopulate(coverFile, items);
|
_updateAlbumPostPopulate(items);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateAlbumPostPopulate(File coverFile, List<AlbumItem> items) {
|
void _updateAlbumPostPopulate(List<AlbumItem> items) {
|
||||||
List<File> timeDescSortedFiles;
|
List<File> timeDescSortedFiles;
|
||||||
if (widget.album.sortProvider is AlbumTimeSortProvider) {
|
if (widget.album.sortProvider is AlbumTimeSortProvider) {
|
||||||
if ((widget.album.sortProvider as AlbumTimeSortProvider).isAscending) {
|
if ((widget.album.sortProvider as AlbumTimeSortProvider).isAscending) {
|
||||||
|
@ -168,7 +173,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
|
|
||||||
bool shouldUpdate = false;
|
bool shouldUpdate = false;
|
||||||
final albumUpdatedCover = UpdateDynamicAlbumCover()
|
final albumUpdatedCover = UpdateDynamicAlbumCover()
|
||||||
.updateWithSortedFiles(_album, timeDescSortedFiles);
|
.updateWithSortedFiles(_album!, timeDescSortedFiles);
|
||||||
if (!identical(albumUpdatedCover, _album)) {
|
if (!identical(albumUpdatedCover, _album)) {
|
||||||
_log.info("[_updateAlbumPostPopulate] Update album cover");
|
_log.info("[_updateAlbumPostPopulate] Update album cover");
|
||||||
shouldUpdate = true;
|
shouldUpdate = true;
|
||||||
|
@ -176,7 +181,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
_album = albumUpdatedCover;
|
_album = albumUpdatedCover;
|
||||||
|
|
||||||
final albumUpdatedTime = UpdateDynamicAlbumTime()
|
final albumUpdatedTime = UpdateDynamicAlbumTime()
|
||||||
.updateWithSortedFiles(_album, timeDescSortedFiles);
|
.updateWithSortedFiles(_album!, timeDescSortedFiles);
|
||||||
if (!identical(albumUpdatedTime, _album)) {
|
if (!identical(albumUpdatedTime, _album)) {
|
||||||
_log.info(
|
_log.info(
|
||||||
"[_updateAlbumPostPopulate] Update album time: ${albumUpdatedTime.provider.latestItemTime}");
|
"[_updateAlbumPostPopulate] Update album time: ${albumUpdatedTime.provider.latestItemTime}");
|
||||||
|
@ -185,7 +190,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
_album = albumUpdatedTime;
|
_album = albumUpdatedTime;
|
||||||
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(widget.account, _album);
|
UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(widget.account, _album!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,11 +246,11 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
return buildNormalAppBar(
|
return buildNormalAppBar(
|
||||||
context,
|
context,
|
||||||
widget.account,
|
widget.account,
|
||||||
_album,
|
_album!,
|
||||||
menuItemBuilder: (context) => [
|
menuItemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _menuValueConvertBasic,
|
value: _menuValueConvertBasic,
|
||||||
child: Text(AppLocalizations.of(context).convertBasicAlbumMenuLabel),
|
child: Text(AppLocalizations.of(context)!.convertBasicAlbumMenuLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelectedMenuItem: (option) {
|
onSelectedMenuItem: (option) {
|
||||||
|
@ -267,7 +272,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
if (platform_k.isAndroid)
|
if (platform_k.isAndroid)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_onSelectionAppBarSharePressed(context);
|
_onSelectionAppBarSharePressed(context);
|
||||||
},
|
},
|
||||||
|
@ -277,7 +282,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _SelectionAppBarOption.delete,
|
value: _SelectionAppBarOption.delete,
|
||||||
child: Text(AppLocalizations.of(context).deleteSelectedTooltip),
|
child: Text(AppLocalizations.of(context)!.deleteSelectedTooltip),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (option) {
|
onSelected: (option) {
|
||||||
|
@ -293,7 +298,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
return buildEditAppBar(context, widget.account, widget.album, actions: [
|
return buildEditAppBar(context, widget.account, widget.album, actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.sort_by_alpha),
|
icon: Icon(Icons.sort_by_alpha),
|
||||||
tooltip: AppLocalizations.of(context).sortTooltip,
|
tooltip: AppLocalizations.of(context)!.sortTooltip,
|
||||||
onPressed: _onEditAppBarSortPressed,
|
onPressed: _onEditAppBarSortPressed,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
@ -317,9 +322,9 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)!
|
||||||
.convertBasicAlbumConfirmationDialogTitle),
|
.convertBasicAlbumConfirmationDialogTitle),
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.convertBasicAlbumConfirmationDialogContent),
|
.convertBasicAlbumConfirmationDialogContent),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -341,17 +346,17 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log.info(
|
_log.info(
|
||||||
"[_onAppBarConvertBasicPressed] Converting album '${_album.name}' to static");
|
"[_onAppBarConvertBasicPressed] Converting album '${_album!.name}' to static");
|
||||||
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
final albumRepo = AlbumRepo(AlbumCachedDataSource());
|
||||||
UpdateAlbum(albumRepo)(
|
UpdateAlbum(albumRepo)(
|
||||||
widget.account,
|
widget.account,
|
||||||
_album.copyWith(
|
_album!.copyWith(
|
||||||
provider: AlbumStaticProvider(items: _sortedItems),
|
provider: AlbumStaticProvider(items: _sortedItems),
|
||||||
coverProvider: AlbumAutoCoverProvider(),
|
coverProvider: AlbumAutoCoverProvider(),
|
||||||
),
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.convertBasicAlbumSuccessNotification),
|
.convertBasicAlbumSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -386,7 +391,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
|
|
||||||
void _onSelectionAppBarDeletePressed() async {
|
void _onSelectionAppBarDeletePressed() async {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.deleteSelectedProcessingNotification(selectedListItems.length)),
|
.deleteSelectedProcessingNotification(selectedListItems.length)),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
|
@ -417,12 +422,12 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
if (failures.isEmpty) {
|
if (failures.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).deleteSelectedSuccessNotification),
|
AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.deleteSelectedFailureNotification(failures.length)),
|
.deleteSelectedFailureNotification(failures.length)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -439,14 +444,14 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEditAppBarSortPressed() {
|
void _onEditAppBarSortPressed() {
|
||||||
final sortProvider = _editAlbum.sortProvider;
|
final sortProvider = _editAlbum!.sortProvider;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => FancyOptionPicker(
|
builder: (context) => FancyOptionPicker(
|
||||||
title: AppLocalizations.of(context).sortOptionDialogTitle,
|
title: AppLocalizations.of(context)!.sortOptionDialogTitle,
|
||||||
items: [
|
items: [
|
||||||
FancyOptionPickerItem(
|
FancyOptionPickerItem(
|
||||||
label: AppLocalizations.of(context).sortOptionTimeAscendingLabel,
|
label: AppLocalizations.of(context)!.sortOptionTimeAscendingLabel,
|
||||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||||
sortProvider.isAscending,
|
sortProvider.isAscending,
|
||||||
onSelect: () {
|
onSelect: () {
|
||||||
|
@ -455,7 +460,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FancyOptionPickerItem(
|
FancyOptionPickerItem(
|
||||||
label: AppLocalizations.of(context).sortOptionTimeDescendingLabel,
|
label: AppLocalizations.of(context)!.sortOptionTimeDescendingLabel,
|
||||||
isSelected: sortProvider is AlbumTimeSortProvider &&
|
isSelected: sortProvider is AlbumTimeSortProvider &&
|
||||||
!sortProvider.isAscending,
|
!sortProvider.isAscending,
|
||||||
onSelect: () {
|
onSelect: () {
|
||||||
|
@ -469,7 +474,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSortOldestPressed() {
|
void _onSortOldestPressed() {
|
||||||
_editAlbum = _editAlbum.copyWith(
|
_editAlbum = _editAlbum!.copyWith(
|
||||||
sortProvider: AlbumTimeSortProvider(isAscending: true),
|
sortProvider: AlbumTimeSortProvider(isAscending: true),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -478,7 +483,7 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSortNewestPressed() {
|
void _onSortNewestPressed() {
|
||||||
_editAlbum = _editAlbum.copyWith(
|
_editAlbum = _editAlbum!.copyWith(
|
||||||
sortProvider: AlbumTimeSortProvider(isAscending: false),
|
sortProvider: AlbumTimeSortProvider(isAscending: false),
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -489,9 +494,9 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
void _transformItems(List<AlbumItem> items) {
|
void _transformItems(List<AlbumItem> items) {
|
||||||
if (_editAlbum != null) {
|
if (_editAlbum != null) {
|
||||||
// edit mode
|
// edit mode
|
||||||
_sortedItems = _editAlbum.sortProvider.sort(items);
|
_sortedItems = _editAlbum!.sortProvider.sort(items);
|
||||||
} else {
|
} else {
|
||||||
_sortedItems = _album.sortProvider.sort(items);
|
_sortedItems = _album!.sortProvider.sort(items);
|
||||||
}
|
}
|
||||||
_onSortedItemsUpdated();
|
_onSortedItemsUpdated();
|
||||||
}
|
}
|
||||||
|
@ -535,12 +540,12 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Album _album;
|
Album? _album;
|
||||||
var _sortedItems = <AlbumItem>[];
|
var _sortedItems = <AlbumItem>[];
|
||||||
var _backingFiles = <File>[];
|
var _backingFiles = <File>[];
|
||||||
|
|
||||||
final _editFormKey = GlobalKey<FormState>();
|
final _editFormKey = GlobalKey<FormState>();
|
||||||
Album _editAlbum;
|
Album? _editAlbum;
|
||||||
|
|
||||||
static final _log =
|
static final _log =
|
||||||
Logger("widget.dynamic_album_viewer._DynamicAlbumViewerState");
|
Logger("widget.dynamic_album_viewer._DynamicAlbumViewerState");
|
||||||
|
@ -549,8 +554,8 @@ class _DynamicAlbumViewerState extends State<DynamicAlbumViewer>
|
||||||
|
|
||||||
abstract class _ListItem implements SelectableItem {
|
abstract class _ListItem implements SelectableItem {
|
||||||
_ListItem({
|
_ListItem({
|
||||||
@required this.index,
|
required this.index,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : _onTap = onTap;
|
}) : _onTap = onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -571,14 +576,14 @@ abstract class _ListItem implements SelectableItem {
|
||||||
|
|
||||||
final int index;
|
final int index;
|
||||||
|
|
||||||
final VoidCallback _onTap;
|
final VoidCallback? _onTap;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _FileListItem extends _ListItem {
|
abstract class _FileListItem extends _ListItem {
|
||||||
_FileListItem({
|
_FileListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required this.file,
|
required this.file,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
@ -589,11 +594,11 @@ abstract class _FileListItem extends _ListItem {
|
||||||
|
|
||||||
class _ImageListItem extends _FileListItem {
|
class _ImageListItem extends _FileListItem {
|
||||||
_ImageListItem({
|
_ImageListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
file: file,
|
file: file,
|
||||||
|
@ -615,11 +620,11 @@ class _ImageListItem extends _FileListItem {
|
||||||
|
|
||||||
class _VideoListItem extends _FileListItem {
|
class _VideoListItem extends _FileListItem {
|
||||||
_VideoListItem({
|
_VideoListItem({
|
||||||
@required int index,
|
required int index,
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(
|
}) : super(
|
||||||
index: index,
|
index: index,
|
||||||
file: file,
|
file: file,
|
||||||
|
|
|
@ -3,28 +3,28 @@ import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class FancyOptionPickerItem {
|
class FancyOptionPickerItem {
|
||||||
FancyOptionPickerItem({
|
FancyOptionPickerItem({
|
||||||
@required this.label,
|
required this.label,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
String label;
|
String label;
|
||||||
bool isSelected;
|
bool isSelected;
|
||||||
VoidCallback onSelect;
|
VoidCallback? onSelect;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fancy looking dialog to pick an option
|
/// A fancy looking dialog to pick an option
|
||||||
class FancyOptionPicker extends StatelessWidget {
|
class FancyOptionPicker extends StatelessWidget {
|
||||||
FancyOptionPicker({
|
FancyOptionPicker({
|
||||||
Key key,
|
Key? key,
|
||||||
this.title,
|
this.title,
|
||||||
@required this.items,
|
required this.items,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
return SimpleDialog(
|
return SimpleDialog(
|
||||||
title: title != null ? Text(title) : null,
|
title: title != null ? Text(title!) : null,
|
||||||
children: items
|
children: items
|
||||||
.map((e) => SimpleDialogOption(
|
.map((e) => SimpleDialogOption(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
@ -47,6 +47,6 @@ class FancyOptionPicker extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String title;
|
final String? title;
|
||||||
final List<FancyOptionPickerItem> items;
|
final List<FancyOptionPickerItem> items;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,16 @@ class HomeArguments {
|
||||||
class Home extends StatefulWidget {
|
class Home extends StatefulWidget {
|
||||||
static const routeName = "/home";
|
static const routeName = "/home";
|
||||||
|
|
||||||
|
static Route buildRoute(HomeArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => Home.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
Home({
|
Home({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Home.fromArgs(HomeArguments args, {Key key})
|
Home.fromArgs(HomeArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
account: args.account,
|
account: args.account,
|
||||||
);
|
);
|
||||||
|
@ -33,12 +37,6 @@ class Home extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeState extends State<Home> {
|
class _HomeState extends State<Home> {
|
||||||
@override
|
|
||||||
initState() {
|
|
||||||
super.initState();
|
|
||||||
_pageController = PageController(initialPage: 0, keepPage: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
return AppTheme(
|
return AppTheme(
|
||||||
|
@ -54,11 +52,11 @@ class _HomeState extends State<Home> {
|
||||||
items: <BottomNavigationBarItem>[
|
items: <BottomNavigationBarItem>[
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: const Icon(Icons.photo_outlined),
|
icon: const Icon(Icons.photo_outlined),
|
||||||
label: AppLocalizations.of(context).photosTabLabel,
|
label: AppLocalizations.of(context)!.photosTabLabel,
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: const Icon(Icons.photo_album_outlined),
|
icon: const Icon(Icons.photo_album_outlined),
|
||||||
label: AppLocalizations.of(context).albumsTabLabel,
|
label: AppLocalizations.of(context)!.albumsTabLabel,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
currentIndex: _nextPage,
|
currentIndex: _nextPage,
|
||||||
|
@ -108,6 +106,6 @@ class _HomeState extends State<Home> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
PageController _pageController;
|
final _pageController = PageController(initialPage: 0, keepPage: false);
|
||||||
int _nextPage = 0;
|
int _nextPage = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,8 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class HomeAlbums extends StatefulWidget {
|
class HomeAlbums extends StatefulWidget {
|
||||||
HomeAlbums({
|
HomeAlbums({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -83,7 +83,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
_reqQuery();
|
_reqQuery();
|
||||||
} else {
|
} else {
|
||||||
// process the current state
|
// process the current state
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onStateChange(context, _bloc.state);
|
_onStateChange(context, _bloc.state);
|
||||||
});
|
});
|
||||||
|
@ -149,12 +149,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)!
|
||||||
.selectionAppBarTitle(_selectedItems.length)),
|
.selectionAppBarTitle(_selectedItems.length)),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
tooltip: AppLocalizations.of(context).deleteSelectedTooltip,
|
tooltip: AppLocalizations.of(context)!.deleteSelectedTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_onSelectionAppBarDeletePressed();
|
_onSelectionAppBarDeletePressed();
|
||||||
},
|
},
|
||||||
|
@ -171,13 +171,13 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => _onSearchPressed(context),
|
onPressed: () => _onSearchPressed(context),
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
tooltip: AppLocalizations.of(context).searchTooltip,
|
tooltip: AppLocalizations.of(context)!.searchTooltip,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
menuActions: [
|
menuActions: [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _menuValueImport,
|
value: _menuValueImport,
|
||||||
child: Text(AppLocalizations.of(context).importFoldersTooltip),
|
child: Text(AppLocalizations.of(context)!.importFoldersTooltip),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelectedMenuActions: (option) {
|
onSelectedMenuActions: (option) {
|
||||||
|
@ -225,7 +225,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: AppLocalizations.of(context).albumArchiveLabel,
|
title: AppLocalizations.of(context)!.albumArchiveLabel,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pushNamed(ArchiveViewer.routeName,
|
Navigator.of(context).pushNamed(ArchiveViewer.routeName,
|
||||||
arguments: ArchiveViewerArguments(widget.account));
|
arguments: ArchiveViewerArguments(widget.account));
|
||||||
|
@ -247,7 +247,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: AppLocalizations.of(context).createAlbumTooltip,
|
title: AppLocalizations.of(context)!.createAlbumTooltip,
|
||||||
onTap: () => _onNewAlbumItemTap(context),
|
onTap: () => _onNewAlbumItemTap(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -324,7 +324,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
"[_onNewAlbumItemTap] Failed while showDialog", e, stacktrace);
|
"[_onNewAlbumItemTap] Failed while showDialog", e, stacktrace);
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content:
|
||||||
Text(AppLocalizations.of(context).createAlbumFailureNotification),
|
Text(AppLocalizations.of(context)!.createAlbumFailureNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
@ -337,11 +337,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
|
|
||||||
Future<void> _onSelectionAppBarDeletePressed() async {
|
Future<void> _onSelectionAppBarDeletePressed() async {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.deleteSelectedProcessingNotification(_selectedItems.length)),
|
.deleteSelectedProcessingNotification(_selectedItems.length)),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
final selectedFiles = _selectedItems.map((e) => e.album.albumFile).toList();
|
final selectedFiles =
|
||||||
|
_selectedItems.map((e) => e.album.albumFile!).toList();
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedItems.clear();
|
_selectedItems.clear();
|
||||||
});
|
});
|
||||||
|
@ -363,12 +364,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
if (failures.isEmpty) {
|
if (failures.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).deleteSelectedSuccessNotification),
|
AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.deleteSelectedFailureNotification(failures.length)),
|
.deleteSelectedFailureNotification(failures.length)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -411,12 +412,12 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
.map((from) {
|
.map((from) {
|
||||||
try {
|
try {
|
||||||
return _items.whereType<_GridItem>().firstWhere(
|
return _items.whereType<_GridItem>().firstWhere(
|
||||||
(to) => from.album.albumFile.path == to.album.albumFile.path);
|
(to) => from.album.albumFile!.path == to.album.albumFile!.path);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.where((element) => element != null)
|
.whereType<_GridItem>()
|
||||||
.toList();
|
.toList();
|
||||||
_selectedItems
|
_selectedItems
|
||||||
..clear()
|
..clear()
|
||||||
|
@ -439,7 +440,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
|
|
||||||
bool get _isSelectionMode => _selectedItems.isNotEmpty;
|
bool get _isSelectionMode => _selectedItems.isNotEmpty;
|
||||||
|
|
||||||
ListAlbumBloc _bloc;
|
late ListAlbumBloc _bloc;
|
||||||
|
|
||||||
final _items = <_GridItem>[];
|
final _items = <_GridItem>[];
|
||||||
final _selectedItems = <_GridItem>[];
|
final _selectedItems = <_GridItem>[];
|
||||||
|
|
|
@ -13,8 +13,8 @@ import 'package:nc_photos/widget/settings.dart';
|
||||||
/// AppBar for home screens
|
/// AppBar for home screens
|
||||||
class HomeSliverAppBar extends StatelessWidget {
|
class HomeSliverAppBar extends StatelessWidget {
|
||||||
HomeSliverAppBar({
|
HomeSliverAppBar({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
this.actions,
|
this.actions,
|
||||||
this.menuActions,
|
this.menuActions,
|
||||||
this.onSelectedMenuActions,
|
this.onSelectedMenuActions,
|
||||||
|
@ -90,7 +90,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
||||||
inactiveThumbImage:
|
inactiveThumbImage:
|
||||||
const AssetImage("assets/ic_dark_mode_switch_24dp.png"),
|
const AssetImage("assets/ic_dark_mode_switch_24dp.png"),
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton<int>(
|
||||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
itemBuilder: (context) =>
|
itemBuilder: (context) =>
|
||||||
(menuActions ?? []) +
|
(menuActions ?? []) +
|
||||||
|
@ -98,7 +98,7 @@ class HomeSliverAppBar extends StatelessWidget {
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _menuValueAbout,
|
value: _menuValueAbout,
|
||||||
child:
|
child:
|
||||||
Text(AppLocalizations.of(context).settingsMenuLabel),
|
Text(AppLocalizations.of(context)!.settingsMenuLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (option) {
|
onSelected: (option) {
|
||||||
|
@ -125,12 +125,12 @@ class HomeSliverAppBar extends StatelessWidget {
|
||||||
final Account account;
|
final Account account;
|
||||||
|
|
||||||
/// Screen specific action buttons
|
/// Screen specific action buttons
|
||||||
final List<Widget> actions;
|
final List<Widget>? actions;
|
||||||
|
|
||||||
/// Screen specific actions under the overflow menu. The value of each item
|
/// Screen specific actions under the overflow menu. The value of each item
|
||||||
/// much >= 0
|
/// much >= 0
|
||||||
final List<PopupMenuEntry<int>> menuActions;
|
final List<PopupMenuEntry<int>>? menuActions;
|
||||||
final void Function(int) onSelectedMenuActions;
|
final void Function(int)? onSelectedMenuActions;
|
||||||
|
|
||||||
static const _menuValueAbout = -1;
|
static const _menuValueAbout = -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,8 @@ import 'package:nc_photos/widget/viewer.dart';
|
||||||
|
|
||||||
class HomePhotos extends StatefulWidget {
|
class HomePhotos extends StatefulWidget {
|
||||||
HomePhotos({
|
HomePhotos({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -59,7 +59,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevel(0);
|
_thumbZoomLevel = Pref.inst().getHomePhotosZoomLevelOr(0);
|
||||||
_initBloc();
|
_initBloc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
_reqQuery();
|
_reqQuery();
|
||||||
} else {
|
} else {
|
||||||
// process the current state
|
// process the current state
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onStateChange(context, _bloc.state);
|
_onStateChange(context, _bloc.state);
|
||||||
});
|
});
|
||||||
|
@ -162,35 +162,36 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)!
|
||||||
.selectionAppBarTitle(selectedListItems.length)),
|
.selectionAppBarTitle(selectedListItems.length)),
|
||||||
actions: [
|
actions: [
|
||||||
if (platform_k.isAndroid)
|
if (platform_k.isAndroid)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
tooltip: AppLocalizations.of(context).shareSelectedTooltip,
|
tooltip: AppLocalizations.of(context)!.shareSelectedTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_onSelectionAppBarSharePressed(context);
|
_onSelectionAppBarSharePressed(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.playlist_add),
|
icon: const Icon(Icons.playlist_add),
|
||||||
tooltip: AppLocalizations.of(context).addSelectedToAlbumTooltip,
|
tooltip: AppLocalizations.of(context)!.addSelectedToAlbumTooltip,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_onSelectionAppBarAddToAlbumPressed(context);
|
_onSelectionAppBarAddToAlbumPressed(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton<_SelectionAppBarMenuOption>(
|
||||||
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _SelectionAppBarMenuOption.archive,
|
value: _SelectionAppBarMenuOption.archive,
|
||||||
child:
|
child: Text(
|
||||||
Text(AppLocalizations.of(context).archiveSelectedMenuLabel),
|
AppLocalizations.of(context)!.archiveSelectedMenuLabel),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _SelectionAppBarMenuOption.delete,
|
value: _SelectionAppBarMenuOption.delete,
|
||||||
child: Text(AppLocalizations.of(context).deleteSelectedTooltip),
|
child:
|
||||||
|
Text(AppLocalizations.of(context)!.deleteSelectedTooltip),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (option) {
|
onSelected: (option) {
|
||||||
|
@ -212,7 +213,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
actions: [
|
actions: [
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Icons.photo_size_select_large),
|
icon: const Icon(Icons.photo_size_select_large),
|
||||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
tooltip: AppLocalizations.of(context)!.zoomTooltip,
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuZoom(
|
PopupMenuZoom(
|
||||||
initialValue: _thumbZoomLevel,
|
initialValue: _thumbZoomLevel,
|
||||||
|
@ -231,7 +232,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
menuActions: [
|
menuActions: [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _menuValueRefresh,
|
value: _menuValueRefresh,
|
||||||
child: Text(AppLocalizations.of(context).refreshMenuLabel),
|
child: Text(AppLocalizations.of(context)!.refreshMenuLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelectedMenuActions: (option) {
|
onSelectedMenuActions: (option) {
|
||||||
|
@ -251,7 +252,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
} else if (state is ScanDirBlocSuccess || state is ScanDirBlocLoading) {
|
} else if (state is ScanDirBlocSuccess || state is ScanDirBlocLoading) {
|
||||||
_transformItems(state.files);
|
_transformItems(state.files);
|
||||||
if (state is ScanDirBlocSuccess) {
|
if (state is ScanDirBlocSuccess) {
|
||||||
if (Pref.inst().isEnableExif() && !_hasFiredMetadataTask.value) {
|
if (Pref.inst().isEnableExifOr() && !_hasFiredMetadataTask.value) {
|
||||||
KiwiContainer()
|
KiwiContainer()
|
||||||
.resolve<MetadataTaskManager>()
|
.resolve<MetadataTaskManager>()
|
||||||
.addTask(MetadataTask(widget.account));
|
.addTask(MetadataTask(widget.account));
|
||||||
|
@ -306,14 +307,14 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
clearSelectedItems();
|
clearSelectedItems();
|
||||||
});
|
});
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.addSelectedToAlbumSuccessNotification(value.name)),
|
.addSelectedToAlbumSuccessNotification(value.name)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
}).catchError((_) {});
|
}).catchError((_) {});
|
||||||
} else {
|
} else {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.addSelectedToAlbumFailureNotification),
|
.addSelectedToAlbumFailureNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -325,7 +326,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
stacktrace);
|
stacktrace);
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"${AppLocalizations.of(context).addSelectedToAlbumFailureNotification}: "
|
"${AppLocalizations.of(context)!.addSelectedToAlbumFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -355,7 +356,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
"[_addSelectedToAlbum] Failed while updating album", e, stacktrace);
|
"[_addSelectedToAlbum] Failed while updating album", e, stacktrace);
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"${AppLocalizations.of(context).addSelectedToAlbumFailureNotification}: "
|
"${AppLocalizations.of(context)!.addSelectedToAlbumFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -365,7 +366,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
|
|
||||||
Future<void> _onSelectionAppBarDeletePressed(BuildContext context) async {
|
Future<void> _onSelectionAppBarDeletePressed(BuildContext context) async {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.deleteSelectedProcessingNotification(selectedListItems.length)),
|
.deleteSelectedProcessingNotification(selectedListItems.length)),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
|
@ -394,12 +395,12 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
if (failures.isEmpty) {
|
if (failures.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).deleteSelectedSuccessNotification),
|
AppLocalizations.of(context)!.deleteSelectedSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.deleteSelectedFailureNotification(failures.length)),
|
.deleteSelectedFailureNotification(failures.length)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -425,7 +426,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
|
|
||||||
Future<void> _onSelectionAppBarArchivePressed(BuildContext context) async {
|
Future<void> _onSelectionAppBarArchivePressed(BuildContext context) async {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.archiveSelectedProcessingNotification(selectedListItems.length)),
|
.archiveSelectedProcessingNotification(selectedListItems.length)),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
|
@ -454,12 +455,12 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
if (failures.isEmpty) {
|
if (failures.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).archiveSelectedSuccessNotification),
|
AppLocalizations.of(context)!.archiveSelectedSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.archiveSelectedFailureNotification(failures.length)),
|
.archiveSelectedFailureNotification(failures.length)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -478,16 +479,16 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
file_util.isSupportedFormat(element) && element.isArchived != true)
|
file_util.isSupportedFormat(element) && element.isArchived != true)
|
||||||
.sorted(compareFileDateTimeDescending);
|
.sorted(compareFileDateTimeDescending);
|
||||||
|
|
||||||
DateTime currentDate;
|
DateTime? currentDate;
|
||||||
final isMonthOnly = _thumbZoomLevel < 0;
|
final isMonthOnly = _thumbZoomLevel < 0;
|
||||||
itemStreamListItems = () sync* {
|
itemStreamListItems = () sync* {
|
||||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||||
final f = _backingFiles[i];
|
final f = _backingFiles[i];
|
||||||
|
|
||||||
final newDate = f.bestDateTime?.toLocal();
|
final newDate = f.bestDateTime.toLocal();
|
||||||
if (newDate?.year != currentDate?.year ||
|
if (newDate.year != currentDate?.year ||
|
||||||
newDate?.month != currentDate?.month ||
|
newDate.month != currentDate?.month ||
|
||||||
(!isMonthOnly && newDate?.day != currentDate?.day)) {
|
(!isMonthOnly && newDate.day != currentDate?.day)) {
|
||||||
yield _DateListItem(date: newDate, isMonthOnly: isMonthOnly);
|
yield _DateListItem(date: newDate, isMonthOnly: isMonthOnly);
|
||||||
currentDate = newDate;
|
currentDate = newDate;
|
||||||
}
|
}
|
||||||
|
@ -546,13 +547,13 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the estimated scroll extent of the custom scroll view, or null
|
/// Return the estimated scroll extent of the custom scroll view, or null
|
||||||
double _getScrollViewExtent(BoxConstraints constraints) {
|
double? _getScrollViewExtent(BoxConstraints constraints) {
|
||||||
if (_itemListMaxExtent != null &&
|
if (_itemListMaxExtent != null &&
|
||||||
constraints.hasBoundedHeight &&
|
constraints.hasBoundedHeight &&
|
||||||
_appBarExtent != null) {
|
_appBarExtent != null) {
|
||||||
// scroll extent = list height - widget viewport height + sliver app bar height + list padding
|
// scroll extent = list height - widget viewport height + sliver app bar height + list padding
|
||||||
final scrollExtent =
|
final scrollExtent =
|
||||||
_itemListMaxExtent - constraints.maxHeight + _appBarExtent + 16;
|
_itemListMaxExtent! - constraints.maxHeight + _appBarExtent! + 16;
|
||||||
_log.info(
|
_log.info(
|
||||||
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + 16 = $scrollExtent");
|
"[_getScrollViewExtent] $_itemListMaxExtent - ${constraints.maxHeight} + $_appBarExtent + 16 = $scrollExtent");
|
||||||
return scrollExtent;
|
return scrollExtent;
|
||||||
|
@ -595,7 +596,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanDirBloc _bloc;
|
late ScanDirBloc _bloc;
|
||||||
|
|
||||||
var _backingFiles = <File>[];
|
var _backingFiles = <File>[];
|
||||||
|
|
||||||
|
@ -603,8 +604,8 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
double _appBarExtent;
|
double? _appBarExtent;
|
||||||
double _itemListMaxExtent;
|
double? _itemListMaxExtent;
|
||||||
|
|
||||||
static final _log = Logger("widget.home_photos._HomePhotosState");
|
static final _log = Logger("widget.home_photos._HomePhotosState");
|
||||||
static const _menuValueRefresh = 0;
|
static const _menuValueRefresh = 0;
|
||||||
|
@ -612,7 +613,7 @@ class _HomePhotosState extends State<HomePhotos>
|
||||||
|
|
||||||
abstract class _ListItem implements SelectableItem {
|
abstract class _ListItem implements SelectableItem {
|
||||||
_ListItem({
|
_ListItem({
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : _onTap = onTap;
|
}) : _onTap = onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -624,12 +625,12 @@ abstract class _ListItem implements SelectableItem {
|
||||||
@override
|
@override
|
||||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||||
|
|
||||||
final VoidCallback _onTap;
|
final VoidCallback? _onTap;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DateListItem extends _ListItem {
|
class _DateListItem extends _ListItem {
|
||||||
_DateListItem({
|
_DateListItem({
|
||||||
@required this.date,
|
required this.date,
|
||||||
this.isMonthOnly = false,
|
this.isMonthOnly = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -647,7 +648,7 @@ class _DateListItem extends _ListItem {
|
||||||
isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY;
|
isMonthOnly ? DateFormat.YEAR_MONTH : DateFormat.YEAR_MONTH_DAY;
|
||||||
subtitle =
|
subtitle =
|
||||||
DateFormat(pattern, Localizations.localeOf(context).languageCode)
|
DateFormat(pattern, Localizations.localeOf(context).languageCode)
|
||||||
.format(date.toLocal());
|
.format(date!.toLocal());
|
||||||
}
|
}
|
||||||
return Align(
|
return Align(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
@ -655,7 +656,7 @@ class _DateListItem extends _ListItem {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Text(
|
child: Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style: Theme.of(context).textTheme.caption.copyWith(
|
style: Theme.of(context).textTheme.caption!.copyWith(
|
||||||
color: AppTheme.getPrimaryTextColor(context),
|
color: AppTheme.getPrimaryTextColor(context),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
@ -664,14 +665,14 @@ class _DateListItem extends _ListItem {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final DateTime date;
|
final DateTime? date;
|
||||||
final bool isMonthOnly;
|
final bool isMonthOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _FileListItem extends _ListItem {
|
abstract class _FileListItem extends _ListItem {
|
||||||
_FileListItem({
|
_FileListItem({
|
||||||
@required this.file,
|
required this.file,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(onTap: onTap);
|
}) : super(onTap: onTap);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -687,10 +688,10 @@ abstract class _FileListItem extends _ListItem {
|
||||||
|
|
||||||
class _ImageListItem extends _FileListItem {
|
class _ImageListItem extends _FileListItem {
|
||||||
_ImageListItem({
|
_ImageListItem({
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(file: file, onTap: onTap);
|
}) : super(file: file, onTap: onTap);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -708,10 +709,10 @@ class _ImageListItem extends _FileListItem {
|
||||||
|
|
||||||
class _VideoListItem extends _FileListItem {
|
class _VideoListItem extends _FileListItem {
|
||||||
_VideoListItem({
|
_VideoListItem({
|
||||||
@required File file,
|
required File file,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback? onTap,
|
||||||
}) : super(file: file, onTap: onTap);
|
}) : super(file: file, onTap: onTap);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -11,9 +11,9 @@ import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
|
||||||
|
|
||||||
class ImageViewer extends StatefulWidget {
|
class ImageViewer extends StatefulWidget {
|
||||||
ImageViewer({
|
ImageViewer({
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.file,
|
required this.file,
|
||||||
this.canZoom,
|
required this.canZoom,
|
||||||
this.onLoaded,
|
this.onLoaded,
|
||||||
this.onHeightChanged,
|
this.onHeightChanged,
|
||||||
this.onZoomStarted,
|
this.onZoomStarted,
|
||||||
|
@ -35,10 +35,10 @@ class ImageViewer extends StatefulWidget {
|
||||||
final Account account;
|
final Account account;
|
||||||
final File file;
|
final File file;
|
||||||
final bool canZoom;
|
final bool canZoom;
|
||||||
final VoidCallback onLoaded;
|
final VoidCallback? onLoaded;
|
||||||
final void Function(double height) onHeightChanged;
|
final ValueChanged<double>? onHeightChanged;
|
||||||
final VoidCallback onZoomStarted;
|
final VoidCallback? onZoomStarted;
|
||||||
final VoidCallback onZoomEnded;
|
final VoidCallback? onZoomEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImageViewerState extends State<ImageViewer>
|
class _ImageViewerState extends State<ImageViewer>
|
||||||
|
@ -58,9 +58,9 @@ class _ImageViewerState extends State<ImageViewer>
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: NotificationListener<SizeChangedLayoutNotification>(
|
child: NotificationListener<SizeChangedLayoutNotification>(
|
||||||
onNotification: (_) {
|
onNotification: (_) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
if (_key.currentContext != null) {
|
if (_key.currentContext != null) {
|
||||||
widget.onHeightChanged?.call(_key.currentContext.size.height);
|
widget.onHeightChanged?.call(_key.currentContext!.size!.height);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
@ -78,7 +78,7 @@ class _ImageViewerState extends State<ImageViewer>
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||||
imageBuilder: (context, child, imageProvider) {
|
imageBuilder: (context, child, imageProvider) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
_onItemLoaded();
|
_onItemLoaded();
|
||||||
});
|
});
|
||||||
SizeChangedLayoutNotification().dispatch(context);
|
SizeChangedLayoutNotification().dispatch(context);
|
||||||
|
|
|
@ -10,11 +10,11 @@ abstract class MeasurableItemListState {
|
||||||
|
|
||||||
class MeasurableItemList extends StatefulWidget {
|
class MeasurableItemList extends StatefulWidget {
|
||||||
MeasurableItemList({
|
MeasurableItemList({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.maxCrossAxisExtent,
|
required this.maxCrossAxisExtent,
|
||||||
@required this.itemCount,
|
required this.itemCount,
|
||||||
@required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
@required this.staggeredTileBuilder,
|
required this.staggeredTileBuilder,
|
||||||
this.onMaxExtentChanged,
|
this.onMaxExtentChanged,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class MeasurableItemList extends StatefulWidget {
|
||||||
final int itemCount;
|
final int itemCount;
|
||||||
final IndexedWidgetBuilder itemBuilder;
|
final IndexedWidgetBuilder itemBuilder;
|
||||||
final IndexedStaggeredTileBuilder staggeredTileBuilder;
|
final IndexedStaggeredTileBuilder staggeredTileBuilder;
|
||||||
final ValueChanged<double> onMaxExtentChanged;
|
final ValueChanged<double?>? onMaxExtentChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MeasurableItemListState extends State<MeasurableItemList>
|
class _MeasurableItemListState extends State<MeasurableItemList>
|
||||||
|
@ -35,21 +35,21 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
_prevOrientation = MediaQuery.of(context).orientation;
|
_prevOrientation = MediaQuery.of(context).orientation;
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance!.addObserver(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
dispose() {
|
dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance!.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
didChangeMetrics() {
|
didChangeMetrics() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
final orientation = MediaQuery.of(context).orientation;
|
final orientation = MediaQuery.of(context).orientation;
|
||||||
if (orientation != _prevOrientation) {
|
if (orientation != _prevOrientation) {
|
||||||
_log.info(
|
_log.info(
|
||||||
|
@ -70,7 +70,8 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
||||||
}
|
}
|
||||||
if (constraints.crossAxisExtent != _prevListWidth) {
|
if (constraints.crossAxisExtent != _prevListWidth) {
|
||||||
_log.info("[build] updateListHeight: list viewport width changed");
|
_log.info("[build] updateListHeight: list viewport width changed");
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight());
|
WidgetsBinding.instance!
|
||||||
|
.addPostFrameCallback((_) => updateListHeight());
|
||||||
_prevListWidth = constraints.crossAxisExtent;
|
_prevListWidth = constraints.crossAxisExtent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +79,8 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
||||||
final cellSize = widget.maxCrossAxisExtent;
|
final cellSize = widget.maxCrossAxisExtent;
|
||||||
if (cellSize != _prevCellSize) {
|
if (cellSize != _prevCellSize) {
|
||||||
_log.info("[build] updateListHeight: cell size changed");
|
_log.info("[build] updateListHeight: cell size changed");
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight());
|
WidgetsBinding.instance!
|
||||||
|
.addPostFrameCallback((_) => updateListHeight());
|
||||||
_prevCellSize = cellSize;
|
_prevCellSize = cellSize;
|
||||||
}
|
}
|
||||||
_gridKey = _GridKey("$_uniqueToken $cellSize");
|
_gridKey = _GridKey("$_uniqueToken $cellSize");
|
||||||
|
@ -94,9 +96,9 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
updateListHeight() {
|
updateListHeight() {
|
||||||
double newMaxExtent;
|
double? newMaxExtent;
|
||||||
try {
|
try {
|
||||||
final renderObj = _gridKey.currentContext.findRenderObject()
|
final renderObj = _gridKey.currentContext!.findRenderObject()
|
||||||
as RenderMeasurableSliverStaggeredGrid;
|
as RenderMeasurableSliverStaggeredGrid;
|
||||||
final maxExtent = renderObj.calculateExtent();
|
final maxExtent = renderObj.calculateExtent();
|
||||||
_log.info("[updateListHeight] Max extent: $maxExtent");
|
_log.info("[updateListHeight] Max extent: $maxExtent");
|
||||||
|
@ -118,14 +120,14 @@ class _MeasurableItemListState extends State<MeasurableItemList>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double _prevListWidth;
|
double? _prevListWidth;
|
||||||
double _prevCellSize;
|
double? _prevCellSize;
|
||||||
double _maxExtent;
|
double? _maxExtent;
|
||||||
Orientation _prevOrientation;
|
Orientation? _prevOrientation;
|
||||||
|
|
||||||
// this unique token is there to keep the global key unique
|
// this unique token is there to keep the global key unique
|
||||||
final _uniqueToken = Uuid().v4();
|
final _uniqueToken = Uuid().v4();
|
||||||
GlobalObjectKey _gridKey;
|
late GlobalObjectKey _gridKey;
|
||||||
|
|
||||||
static final _log =
|
static final _log =
|
||||||
Logger("widget.measurable_item_list._MeasurableItemListState");
|
Logger("widget.measurable_item_list._MeasurableItemListState");
|
||||||
|
|
|
@ -8,9 +8,9 @@ class MeasureSize extends SingleChildRenderObjectWidget {
|
||||||
final OnWidgetSizeChanged onChange;
|
final OnWidgetSizeChanged onChange;
|
||||||
|
|
||||||
const MeasureSize({
|
const MeasureSize({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.onChange,
|
required this.onChange,
|
||||||
@required Widget child,
|
required Widget child,
|
||||||
}) : super(key: key, child: child);
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -20,7 +20,7 @@ class MeasureSize extends SingleChildRenderObjectWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||||
Size oldSize;
|
Size? oldSize;
|
||||||
final OnWidgetSizeChanged onChange;
|
final OnWidgetSizeChanged onChange;
|
||||||
|
|
||||||
_MeasureSizeRenderObject(this.onChange);
|
_MeasureSizeRenderObject(this.onChange);
|
||||||
|
@ -29,11 +29,11 @@ class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
super.performLayout();
|
super.performLayout();
|
||||||
|
|
||||||
Size newSize = child.size;
|
var newSize = child?.size;
|
||||||
if (oldSize == newSize) return;
|
if (newSize == null || oldSize == newSize) return;
|
||||||
|
|
||||||
oldSize = newSize;
|
oldSize = newSize;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
onChange(newSize);
|
onChange(newSize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,9 @@ class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||||
|
|
||||||
class SliverMeasureExtent extends SingleChildRenderObjectWidget {
|
class SliverMeasureExtent extends SingleChildRenderObjectWidget {
|
||||||
const SliverMeasureExtent({
|
const SliverMeasureExtent({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.onChange,
|
required this.onChange,
|
||||||
@required Widget child,
|
required Widget child,
|
||||||
}) : super(key: key, child: child);
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -61,16 +61,16 @@ class _SliverMeasureExtentRenderObject extends RenderProxySliver {
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
super.performLayout();
|
super.performLayout();
|
||||||
|
|
||||||
double newExent = child.geometry.scrollExtent;
|
var newExent = child?.geometry?.scrollExtent;
|
||||||
if (_oldExtent == newExent) {
|
if (newExent == null || _oldExtent == newExent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_oldExtent = newExent;
|
_oldExtent = newExent;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => onChange(newExent));
|
WidgetsBinding.instance!.addPostFrameCallback((_) => onChange(newExent));
|
||||||
}
|
}
|
||||||
|
|
||||||
final void Function(double) onChange;
|
final void Function(double) onChange;
|
||||||
|
|
||||||
double _oldExtent;
|
double? _oldExtent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid {
|
class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid {
|
||||||
MeasurableSliverStaggeredGrid.extentBuilder({
|
MeasurableSliverStaggeredGrid.extentBuilder({
|
||||||
Key key,
|
Key? key,
|
||||||
@required double maxCrossAxisExtent,
|
required double maxCrossAxisExtent,
|
||||||
@required IndexedStaggeredTileBuilder staggeredTileBuilder,
|
required IndexedStaggeredTileBuilder staggeredTileBuilder,
|
||||||
@required IndexedWidgetBuilder itemBuilder,
|
required IndexedWidgetBuilder itemBuilder,
|
||||||
@required int itemCount,
|
required int itemCount,
|
||||||
double mainAxisSpacing = 0,
|
double mainAxisSpacing = 0,
|
||||||
double crossAxisSpacing = 0,
|
double crossAxisSpacing = 0,
|
||||||
}) : super(
|
}) : super(
|
||||||
|
@ -32,19 +32,19 @@ class MeasurableSliverStaggeredGrid extends SliverStaggeredGrid {
|
||||||
final element = context as SliverVariableSizeBoxAdaptorElement;
|
final element = context as SliverVariableSizeBoxAdaptorElement;
|
||||||
_renderObject = RenderMeasurableSliverStaggeredGrid(
|
_renderObject = RenderMeasurableSliverStaggeredGrid(
|
||||||
childManager: element, gridDelegate: gridDelegate);
|
childManager: element, gridDelegate: gridDelegate);
|
||||||
return _renderObject;
|
return _renderObject!;
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderMeasurableSliverStaggeredGrid get renderObject => _renderObject;
|
RenderMeasurableSliverStaggeredGrid? get renderObject => _renderObject;
|
||||||
|
|
||||||
RenderMeasurableSliverStaggeredGrid _renderObject;
|
RenderMeasurableSliverStaggeredGrid? _renderObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid
|
class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
RenderMeasurableSliverStaggeredGrid({
|
RenderMeasurableSliverStaggeredGrid({
|
||||||
@required RenderSliverVariableSizeBoxChildManager childManager,
|
required RenderSliverVariableSizeBoxChildManager childManager,
|
||||||
@required SliverStaggeredGridDelegate gridDelegate,
|
required SliverStaggeredGridDelegate gridDelegate,
|
||||||
}) : super(childManager: childManager, gridDelegate: gridDelegate);
|
}) : super(childManager: childManager, gridDelegate: gridDelegate);
|
||||||
|
|
||||||
/// Calculate the height of this staggered grid view
|
/// Calculate the height of this staggered grid view
|
||||||
|
@ -69,13 +69,13 @@ class RenderMeasurableSliverStaggeredGrid extends RenderSliverStaggeredGrid
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool hasTrailingScrollOffset = geometry.hasTrailingScrollOffset;
|
final bool hasTrailingScrollOffset = geometry.hasTrailingScrollOffset;
|
||||||
RenderBox child;
|
RenderBox? child;
|
||||||
if (!hasTrailingScrollOffset) {
|
if (!hasTrailingScrollOffset) {
|
||||||
// Layout the child to compute its tailingScrollOffset.
|
// Layout the child to compute its tailingScrollOffset.
|
||||||
final constraints =
|
final constraints =
|
||||||
BoxConstraints.tightFor(width: geometry.crossAxisExtent);
|
BoxConstraints.tightFor(width: geometry.crossAxisExtent);
|
||||||
child = addAndLayoutChild(index, constraints, parentUsesSize: true);
|
child = addAndLayoutChild(index, constraints, parentUsesSize: true);
|
||||||
geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child));
|
geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child!));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/event/event.dart';
|
import 'package:nc_photos/event/event.dart';
|
||||||
import 'package:nc_photos/language_util.dart' as language_util;
|
import 'package:nc_photos/language_util.dart' as language_util;
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
|
@ -44,10 +45,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
|
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
|
||||||
theme: _getLightTheme(),
|
theme: _getLightTheme(),
|
||||||
darkTheme: _getDarkTheme(),
|
darkTheme: _getDarkTheme(),
|
||||||
themeMode: Pref.inst().isDarkTheme() ? ThemeMode.dark : ThemeMode.light,
|
themeMode:
|
||||||
|
Pref.inst().isDarkThemeOr(false) ? ThemeMode.dark : ThemeMode.light,
|
||||||
initialRoute: Splash.routeName,
|
initialRoute: Splash.routeName,
|
||||||
onGenerateRoute: _onGenerateRoute,
|
onGenerateRoute: _onGenerateRoute,
|
||||||
navigatorObservers: <NavigatorObserver>[MyApp.routeObserver],
|
navigatorObservers: <NavigatorObserver>[MyApp.routeObserver],
|
||||||
|
@ -87,9 +89,9 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
Splash.routeName: (context) => Splash(),
|
Splash.routeName: (context) => Splash(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
|
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
||||||
_log.info("[_onGenerateRoute] Route: ${settings.name}");
|
_log.info("[_onGenerateRoute] Route: ${settings.name}");
|
||||||
Route<dynamic> route;
|
Route<dynamic>? route;
|
||||||
route ??= _handleBasicRoute(settings);
|
route ??= _handleBasicRoute(settings);
|
||||||
route ??= _handleViewerRoute(settings);
|
route ??= _handleViewerRoute(settings);
|
||||||
route ??= _handleConnectRoute(settings);
|
route ??= _handleConnectRoute(settings);
|
||||||
|
@ -112,7 +114,7 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleBasicRoute(RouteSettings settings) {
|
Route<dynamic>? _handleBasicRoute(RouteSettings settings) {
|
||||||
for (final e in _getRouter().entries) {
|
for (final e in _getRouter().entries) {
|
||||||
if (e.key == settings.name) {
|
if (e.key == settings.name) {
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
|
@ -123,13 +125,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleViewerRoute(RouteSettings settings) {
|
Route<dynamic>? _handleViewerRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == Viewer.routeName && settings.arguments != null) {
|
if (settings.name == Viewer.routeName && settings.arguments != null) {
|
||||||
final ViewerArguments args = settings.arguments;
|
final args = settings.arguments as ViewerArguments;
|
||||||
return MaterialPageRoute(
|
return Viewer.buildRoute(args);
|
||||||
builder: (context) => Viewer.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleViewerRoute] Failed while handling route", e);
|
_log.severe("[_handleViewerRoute] Failed while handling route", e);
|
||||||
|
@ -137,13 +137,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleConnectRoute(RouteSettings settings) {
|
Route<dynamic>? _handleConnectRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == Connect.routeName && settings.arguments != null) {
|
if (settings.name == Connect.routeName && settings.arguments != null) {
|
||||||
final ConnectArguments args = settings.arguments;
|
final args = settings.arguments as ConnectArguments;
|
||||||
return MaterialPageRoute(
|
return Connect.buildRoute(args);
|
||||||
builder: (context) => Connect.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleConnectRoute] Failed while handling route", e);
|
_log.severe("[_handleConnectRoute] Failed while handling route", e);
|
||||||
|
@ -151,13 +149,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleHomeRoute(RouteSettings settings) {
|
Route<dynamic>? _handleHomeRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == Home.routeName && settings.arguments != null) {
|
if (settings.name == Home.routeName && settings.arguments != null) {
|
||||||
final HomeArguments args = settings.arguments;
|
final args = settings.arguments as HomeArguments;
|
||||||
return MaterialPageRoute(
|
return Home.buildRoute(args);
|
||||||
builder: (context) => Home.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleHomeRoute] Failed while handling route", e);
|
_log.severe("[_handleHomeRoute] Failed while handling route", e);
|
||||||
|
@ -165,13 +161,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleRootPickerRoute(RouteSettings settings) {
|
Route<dynamic>? _handleRootPickerRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == RootPicker.routeName && settings.arguments != null) {
|
if (settings.name == RootPicker.routeName && settings.arguments != null) {
|
||||||
final RootPickerArguments args = settings.arguments;
|
final args = settings.arguments as RootPickerArguments;
|
||||||
return MaterialPageRoute(
|
return RootPicker.buildRoute(args);
|
||||||
builder: (context) => RootPicker.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleRootPickerRoute] Failed while handling route", e);
|
_log.severe("[_handleRootPickerRoute] Failed while handling route", e);
|
||||||
|
@ -179,14 +173,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleAlbumViewerRoute(RouteSettings settings) {
|
Route<dynamic>? _handleAlbumViewerRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == AlbumViewer.routeName &&
|
if (settings.name == AlbumViewer.routeName &&
|
||||||
settings.arguments != null) {
|
settings.arguments != null) {
|
||||||
final AlbumViewerArguments args = settings.arguments;
|
final args = settings.arguments as AlbumViewerArguments;
|
||||||
return MaterialPageRoute(
|
return AlbumViewer.buildRoute(args);
|
||||||
builder: (context) => AlbumViewer.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleAlbumViewerRoute] Failed while handling route", e);
|
_log.severe("[_handleAlbumViewerRoute] Failed while handling route", e);
|
||||||
|
@ -194,13 +186,11 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleSettingsRoute(RouteSettings settings) {
|
Route<dynamic>? _handleSettingsRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == Settings.routeName && settings.arguments != null) {
|
if (settings.name == Settings.routeName && settings.arguments != null) {
|
||||||
final SettingsArguments args = settings.arguments;
|
final args = settings.arguments as SettingsArguments;
|
||||||
return MaterialPageRoute(
|
return Settings.buildRoute(args);
|
||||||
builder: (context) => Settings.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleSettingsRoute] Failed while handling route", e);
|
_log.severe("[_handleSettingsRoute] Failed while handling route", e);
|
||||||
|
@ -208,14 +198,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleArchiveViewerRoute(RouteSettings settings) {
|
Route<dynamic>? _handleArchiveViewerRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == ArchiveViewer.routeName &&
|
if (settings.name == ArchiveViewer.routeName &&
|
||||||
settings.arguments != null) {
|
settings.arguments != null) {
|
||||||
final ArchiveViewerArguments args = settings.arguments;
|
final args = settings.arguments as ArchiveViewerArguments;
|
||||||
return MaterialPageRoute(
|
return ArchiveViewer.buildRoute(args);
|
||||||
builder: (context) => ArchiveViewer.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleArchiveViewerRoute] Failed while handling route", e);
|
_log.severe("[_handleArchiveViewerRoute] Failed while handling route", e);
|
||||||
|
@ -223,14 +211,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleDynamicAlbumViewerRoute(RouteSettings settings) {
|
Route<dynamic>? _handleDynamicAlbumViewerRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == DynamicAlbumViewer.routeName &&
|
if (settings.name == DynamicAlbumViewer.routeName &&
|
||||||
settings.arguments != null) {
|
settings.arguments != null) {
|
||||||
final DynamicAlbumViewerArguments args = settings.arguments;
|
final args = settings.arguments as DynamicAlbumViewerArguments;
|
||||||
return MaterialPageRoute(
|
return DynamicAlbumViewer.buildRoute(args);
|
||||||
builder: (context) => DynamicAlbumViewer.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe(
|
_log.severe(
|
||||||
|
@ -239,14 +225,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleAlbumDirPickerRoute(RouteSettings settings) {
|
Route<dynamic>? _handleAlbumDirPickerRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == AlbumDirPicker.routeName &&
|
if (settings.name == AlbumDirPicker.routeName &&
|
||||||
settings.arguments != null) {
|
settings.arguments != null) {
|
||||||
final AlbumDirPickerArguments args = settings.arguments;
|
final args = settings.arguments as AlbumDirPickerArguments;
|
||||||
return MaterialPageRoute(
|
return AlbumDirPicker.buildRoute(args);
|
||||||
builder: (context) => AlbumDirPicker.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe(
|
_log.severe(
|
||||||
|
@ -255,14 +239,12 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic> _handleAlbumImporterRoute(RouteSettings settings) {
|
Route<dynamic>? _handleAlbumImporterRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == AlbumImporter.routeName &&
|
if (settings.name == AlbumImporter.routeName &&
|
||||||
settings.arguments != null) {
|
settings.arguments != null) {
|
||||||
final AlbumImporterArguments args = settings.arguments;
|
final args = settings.arguments as AlbumImporterArguments;
|
||||||
return MaterialPageRoute(
|
return AlbumImporter.buildRoute(args);
|
||||||
builder: (context) => AlbumImporter.fromArgs(args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.severe("[_handleAlbumImporterRoute] Failed while handling route", e);
|
_log.severe("[_handleAlbumImporterRoute] Failed while handling route", e);
|
||||||
|
@ -272,8 +254,8 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
|
|
||||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
|
|
||||||
AppEventListener<ThemeChangedEvent> _themeChangedListener;
|
late AppEventListener<ThemeChangedEvent> _themeChangedListener;
|
||||||
AppEventListener<LanguageChangedEvent> _langChangedListener;
|
late AppEventListener<LanguageChangedEvent> _langChangedListener;
|
||||||
|
|
||||||
static final _log = Logger("widget.my_app.MyAppState");
|
static final _log = Logger("widget.my_app.MyAppState");
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/album/cover_provider.dart';
|
import 'package:nc_photos/entity/album/cover_provider.dart';
|
||||||
import 'package:nc_photos/entity/album/provider.dart';
|
import 'package:nc_photos/entity/album/provider.dart';
|
||||||
import 'package:nc_photos/entity/album/sort_provider.dart';
|
import 'package:nc_photos/entity/album/sort_provider.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/use_case/create_album.dart';
|
import 'package:nc_photos/use_case/create_album.dart';
|
||||||
import 'package:nc_photos/widget/album_dir_picker.dart';
|
import 'package:nc_photos/widget/album_dir_picker.dart';
|
||||||
|
|
||||||
|
@ -16,8 +17,8 @@ import 'package:nc_photos/widget/album_dir_picker.dart';
|
||||||
/// cancelled
|
/// cancelled
|
||||||
class NewAlbumDialog extends StatefulWidget {
|
class NewAlbumDialog extends StatefulWidget {
|
||||||
NewAlbumDialog({
|
NewAlbumDialog({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
this.isAllowDynamic = true,
|
this.isAllowDynamic = true,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
||||||
return Visibility(
|
return Visibility(
|
||||||
visible: _isVisible,
|
visible: _isVisible,
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context).createAlbumTooltip),
|
title: Text(AppLocalizations.of(context)!.createAlbumTooltip),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -50,17 +51,17 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(context).nameInputHint,
|
hintText: AppLocalizations.of(context)!.nameInputHint,
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value.isEmpty) {
|
if (value!.isEmpty) {
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.albumNameInputInvalidEmpty;
|
.albumNameInputInvalidEmpty;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.name = value;
|
_formValue.name = value!;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (widget.isAllowDynamic) ...[
|
if (widget.isAllowDynamic) ...[
|
||||||
|
@ -75,7 +76,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (newValue) {
|
onChanged: (newValue) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_provider = newValue;
|
_provider = newValue!;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
|
@ -104,8 +105,8 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onOkPressed(BuildContext context) {
|
void _onOkPressed(BuildContext context) {
|
||||||
if (_formKey.currentState.validate()) {
|
if (_formKey.currentState?.validate() == true) {
|
||||||
_formKey.currentState.save();
|
_formKey.currentState!.save();
|
||||||
if (_formValue.provider == _Provider.static ||
|
if (_formValue.provider == _Provider.static ||
|
||||||
_formValue.provider == null) {
|
_formValue.provider == null) {
|
||||||
_onConfirmStaticAlbum();
|
_onConfirmStaticAlbum();
|
||||||
|
@ -136,7 +137,7 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
||||||
_isVisible = false;
|
_isVisible = false;
|
||||||
});
|
});
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushNamed(AlbumDirPicker.routeName,
|
.pushNamed<List<File>>(AlbumDirPicker.routeName,
|
||||||
arguments: AlbumDirPickerArguments(widget.account))
|
arguments: AlbumDirPickerArguments(widget.account))
|
||||||
.then((value) {
|
.then((value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
|
@ -174,8 +175,8 @@ class _NewAlbumDialogState extends State<NewAlbumDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FormValue {
|
class _FormValue {
|
||||||
String name;
|
late String name;
|
||||||
_Provider provider;
|
_Provider? provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _Provider {
|
enum _Provider {
|
||||||
|
@ -187,10 +188,10 @@ extension on _Provider {
|
||||||
String toValueString(BuildContext context) {
|
String toValueString(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case _Provider.static:
|
case _Provider.static:
|
||||||
return AppLocalizations.of(context).createAlbumDialogBasicLabel;
|
return AppLocalizations.of(context)!.createAlbumDialogBasicLabel;
|
||||||
|
|
||||||
case _Provider.dir:
|
case _Provider.dir:
|
||||||
return AppLocalizations.of(context).createAlbumDialogFolderBasedLabel;
|
return AppLocalizations.of(context)!.createAlbumDialogFolderBasedLabel;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw StateError("Unknown value: $this");
|
throw StateError("Unknown value: $this");
|
||||||
|
@ -200,10 +201,10 @@ extension on _Provider {
|
||||||
String toDescription(BuildContext context) {
|
String toDescription(BuildContext context) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case _Provider.static:
|
case _Provider.static:
|
||||||
return AppLocalizations.of(context).createAlbumDialogBasicDescription;
|
return AppLocalizations.of(context)!.createAlbumDialogBasicDescription;
|
||||||
|
|
||||||
case _Provider.dir:
|
case _Provider.dir:
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.createAlbumDialogFolderBasedDescription;
|
.createAlbumDialogFolderBasedDescription;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -5,7 +5,7 @@ mixin PageVisibilityMixin<T extends StatefulWidget> on State<T>, RouteAware {
|
||||||
@override
|
@override
|
||||||
didChangeDependencies() {
|
didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
MyApp.routeObserver.subscribe(this, ModalRoute.of(context));
|
MyApp.routeObserver.subscribe(this, ModalRoute.of(context)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -6,8 +6,8 @@ import 'package:nc_photos/num_extension.dart';
|
||||||
|
|
||||||
class PhotoDateTimeEditDialog extends StatefulWidget {
|
class PhotoDateTimeEditDialog extends StatefulWidget {
|
||||||
PhotoDateTimeEditDialog({
|
PhotoDateTimeEditDialog({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.initialDateTime,
|
required this.initialDateTime,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -20,7 +20,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context).updateDateTimeDialogTitle),
|
title: Text(AppLocalizations.of(context)!.updateDateTimeDialogTitle),
|
||||||
content: Form(
|
content: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -29,7 +29,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).dateSubtitle,
|
AppLocalizations.of(context)!.dateSubtitle,
|
||||||
style: Theme.of(context).textTheme.subtitle2,
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
@ -38,20 +38,20 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText:
|
||||||
AppLocalizations.of(context).dateYearInputHint,
|
AppLocalizations.of(context)!.dateYearInputHint,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
try {
|
try {
|
||||||
int.parse(value);
|
int.parse(value!);
|
||||||
return null;
|
return null;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.dateTimeInputInvalid;
|
.dateTimeInputInvalid;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.year = int.parse(value);
|
_formValue.year = int.parse(value!);
|
||||||
},
|
},
|
||||||
initialValue: "${widget.initialDateTime.year}",
|
initialValue: "${widget.initialDateTime.year}",
|
||||||
),
|
),
|
||||||
|
@ -62,18 +62,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText:
|
||||||
AppLocalizations.of(context).dateMonthInputHint,
|
AppLocalizations.of(context)!.dateMonthInputHint,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (int.tryParse(value)?.inRange(1, 12) == true) {
|
if (int.tryParse(value!)?.inRange(1, 12) == true) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.dateTimeInputInvalid;
|
.dateTimeInputInvalid;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.month = int.parse(value);
|
_formValue.month = int.parse(value!);
|
||||||
},
|
},
|
||||||
initialValue: widget.initialDateTime.month
|
initialValue: widget.initialDateTime.month
|
||||||
.toString()
|
.toString()
|
||||||
|
@ -85,18 +85,19 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
Flexible(
|
Flexible(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(context).dateDayInputHint,
|
hintText:
|
||||||
|
AppLocalizations.of(context)!.dateDayInputHint,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (int.tryParse(value)?.inRange(1, 31) == true) {
|
if (int.tryParse(value!)?.inRange(1, 31) == true) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.dateTimeInputInvalid;
|
.dateTimeInputInvalid;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.day = int.parse(value);
|
_formValue.day = int.parse(value!);
|
||||||
},
|
},
|
||||||
initialValue:
|
initialValue:
|
||||||
widget.initialDateTime.day.toString().padLeft(2, "0"),
|
widget.initialDateTime.day.toString().padLeft(2, "0"),
|
||||||
|
@ -107,7 +108,7 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).timeSubtitle,
|
AppLocalizations.of(context)!.timeSubtitle,
|
||||||
style: Theme.of(context).textTheme.subtitle2,
|
style: Theme.of(context).textTheme.subtitle2,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
@ -116,18 +117,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText:
|
||||||
AppLocalizations.of(context).timeHourInputHint,
|
AppLocalizations.of(context)!.timeHourInputHint,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (int.tryParse(value)?.inRange(0, 23) == true) {
|
if (int.tryParse(value!)?.inRange(0, 23) == true) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.dateTimeInputInvalid;
|
.dateTimeInputInvalid;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.hour = int.parse(value);
|
_formValue.hour = int.parse(value!);
|
||||||
},
|
},
|
||||||
initialValue: widget.initialDateTime.hour
|
initialValue: widget.initialDateTime.hour
|
||||||
.toString()
|
.toString()
|
||||||
|
@ -140,18 +141,18 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText:
|
hintText:
|
||||||
AppLocalizations.of(context).timeMinuteInputHint,
|
AppLocalizations.of(context)!.timeMinuteInputHint,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (int.tryParse(value)?.inRange(0, 59) == true) {
|
if (int.tryParse(value!)?.inRange(0, 59) == true) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.dateTimeInputInvalid;
|
.dateTimeInputInvalid;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.minute = int.parse(value);
|
_formValue.minute = int.parse(value!);
|
||||||
},
|
},
|
||||||
initialValue: widget.initialDateTime.minute
|
initialValue: widget.initialDateTime.minute
|
||||||
.toString()
|
.toString()
|
||||||
|
@ -180,8 +181,8 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSavePressed(BuildContext context) {
|
void _onSavePressed(BuildContext context) {
|
||||||
if (_formKey.currentState.validate()) {
|
if (_formKey.currentState?.validate() == true) {
|
||||||
_formKey.currentState.save();
|
_formKey.currentState!.save();
|
||||||
final d = DateTime(_formValue.year, _formValue.month, _formValue.day,
|
final d = DateTime(_formValue.year, _formValue.month, _formValue.day,
|
||||||
_formValue.hour, _formValue.minute);
|
_formValue.hour, _formValue.minute);
|
||||||
_log.info("[_onSavePressed] Set date time: $d");
|
_log.info("[_onSavePressed] Set date time: $d");
|
||||||
|
@ -197,9 +198,9 @@ class _PhotoDateTimeEditDialogState extends State<PhotoDateTimeEditDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FormValue {
|
class _FormValue {
|
||||||
int year;
|
late int year;
|
||||||
int month;
|
late int month;
|
||||||
int day;
|
late int day;
|
||||||
int hour;
|
late int hour;
|
||||||
int minute;
|
late int minute;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ import 'package:nc_photos/theme.dart';
|
||||||
|
|
||||||
class PhotoListImage extends StatelessWidget {
|
class PhotoListImage extends StatelessWidget {
|
||||||
const PhotoListImage({
|
const PhotoListImage({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
this.isGif = false,
|
this.isGif = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -72,9 +72,9 @@ class PhotoListImage extends StatelessWidget {
|
||||||
|
|
||||||
class PhotoListVideo extends StatelessWidget {
|
class PhotoListVideo extends StatelessWidget {
|
||||||
const PhotoListVideo({
|
const PhotoListVideo({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.previewUrl,
|
required this.previewUrl,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class PopupMenuZoom extends PopupMenuEntry<void> {
|
class PopupMenuZoom extends PopupMenuEntry<void> {
|
||||||
PopupMenuZoom({
|
PopupMenuZoom({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.initialValue,
|
required this.initialValue,
|
||||||
@required this.minValue,
|
required this.minValue,
|
||||||
@required this.maxValue,
|
required this.maxValue,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class PopupMenuZoom extends PopupMenuEntry<void> {
|
||||||
final int initialValue;
|
final int initialValue;
|
||||||
final double minValue;
|
final double minValue;
|
||||||
final double maxValue;
|
final double maxValue;
|
||||||
final void Function(double) onChanged;
|
final void Function(double)? onChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PopupMenuZoomState extends State<PopupMenuZoom> {
|
class _PopupMenuZoomState extends State<PopupMenuZoom> {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class ProcessingDialog extends StatelessWidget {
|
class ProcessingDialog extends StatelessWidget {
|
||||||
ProcessingDialog({
|
ProcessingDialog({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.text,
|
required this.text,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -25,12 +25,17 @@ class RootPickerArguments {
|
||||||
class RootPicker extends StatefulWidget {
|
class RootPicker extends StatefulWidget {
|
||||||
static const routeName = "/root-picker";
|
static const routeName = "/root-picker";
|
||||||
|
|
||||||
|
static Route buildRoute(RootPickerArguments args) =>
|
||||||
|
MaterialPageRoute<Account>(
|
||||||
|
builder: (context) => RootPicker.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
RootPicker({
|
RootPicker({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
RootPicker.fromArgs(RootPickerArguments args, {Key key})
|
RootPicker.fromArgs(RootPickerArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -100,7 +105,7 @@ class _RootPickerState extends State<RootPicker>
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).rootPickerHeaderText,
|
AppLocalizations.of(context)!.rootPickerHeaderText,
|
||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -108,7 +113,7 @@ class _RootPickerState extends State<RootPicker>
|
||||||
Align(
|
Align(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).rootPickerSubHeaderText,
|
AppLocalizations.of(context)!.rootPickerSubHeaderText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -127,11 +132,11 @@ class _RootPickerState extends State<RootPicker>
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => _onSkipPressed(context),
|
onPressed: () => _onSkipPressed(context),
|
||||||
child: Text(AppLocalizations.of(context).skipButtonLabel),
|
child: Text(AppLocalizations.of(context)!.skipButtonLabel),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => _onConfirmPressed(context),
|
onPressed: () => _onConfirmPressed(context),
|
||||||
child: Text(AppLocalizations.of(context).confirmButtonLabel),
|
child: Text(AppLocalizations.of(context)!.confirmButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -145,7 +150,7 @@ class _RootPickerState extends State<RootPicker>
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.rootPickerSkipConfirmationDialogContent),
|
.rootPickerSkipConfirmationDialogContent),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -174,7 +179,7 @@ class _RootPickerState extends State<RootPicker>
|
||||||
if (roots.isEmpty) {
|
if (roots.isEmpty) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content:
|
||||||
Text(AppLocalizations.of(context).rootPickerListEmptyNotification),
|
Text(AppLocalizations.of(context)!.rootPickerListEmptyNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
|
@ -189,12 +194,12 @@ class _RootPickerState extends State<RootPicker>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_isInitDialogShown = true;
|
_isInitDialogShown = true;
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance!.addPostFrameCallback((_) {
|
||||||
showDialog(
|
showDialog(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ProcessingDialog(
|
builder: (context) => ProcessingDialog(
|
||||||
text: AppLocalizations.of(context).genericProcessingDialogContent),
|
text: AppLocalizations.of(context)!.genericProcessingDialogContent),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import 'package:nc_photos/theme.dart';
|
||||||
// Overlay a check mark if an item is selected
|
// Overlay a check mark if an item is selected
|
||||||
class Selectable extends StatelessWidget {
|
class Selectable extends StatelessWidget {
|
||||||
Selectable({
|
Selectable({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.child,
|
required this.child,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
@required this.iconSize,
|
required this.iconSize,
|
||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
|
@ -55,8 +55,8 @@ class Selectable extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final double iconSize;
|
final double iconSize;
|
||||||
final BorderRadiusGeometry borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
|
|
||||||
final VoidCallback onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import 'package:nc_photos/widget/selectable.dart';
|
||||||
abstract class SelectableItem {
|
abstract class SelectableItem {
|
||||||
Widget buildWidget(BuildContext context);
|
Widget buildWidget(BuildContext context);
|
||||||
|
|
||||||
VoidCallback get onTap => null;
|
VoidCallback? get onTap => null;
|
||||||
bool get isSelectable => false;
|
bool get isSelectable => false;
|
||||||
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||||
@protected
|
@protected
|
||||||
Widget buildItemStreamListOuter(
|
Widget buildItemStreamListOuter(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
@required Widget child,
|
required Widget child,
|
||||||
}) {
|
}) {
|
||||||
if (platform_k.isWeb) {
|
if (platform_k.isWeb) {
|
||||||
// support shift+click group selection on web
|
// support shift+click group selection on web
|
||||||
|
@ -51,8 +51,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Widget buildItemStreamList({
|
Widget buildItemStreamList({
|
||||||
@required double maxCrossAxisExtent,
|
required double maxCrossAxisExtent,
|
||||||
ValueChanged<double> onMaxExtentChanged,
|
ValueChanged<double?>? onMaxExtentChanged,
|
||||||
}) {
|
}) {
|
||||||
return MeasurableItemList(
|
return MeasurableItemList(
|
||||||
key: _listKey,
|
key: _listKey,
|
||||||
|
@ -81,24 +81,25 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||||
@protected
|
@protected
|
||||||
set itemStreamListItems(List<SelectableItem> newItems) {
|
set itemStreamListItems(List<SelectableItem> newItems) {
|
||||||
final lastSelectedItem =
|
final lastSelectedItem =
|
||||||
_lastSelectPosition != null ? _items[_lastSelectPosition] : null;
|
_lastSelectPosition != null ? _items[_lastSelectPosition!] : null;
|
||||||
|
|
||||||
_items = newItems;
|
_items = newItems;
|
||||||
_transformSelectedItems();
|
_transformSelectedItems();
|
||||||
|
|
||||||
// Keep _lastSelectPosition if no changes, drop otherwise
|
// Keep _lastSelectPosition if no changes, drop otherwise
|
||||||
int newLastSelectPosition;
|
int? newLastSelectPosition;
|
||||||
try {
|
try {
|
||||||
if (lastSelectedItem != null &&
|
if (lastSelectedItem != null &&
|
||||||
lastSelectedItem == _items[_lastSelectPosition]) {
|
lastSelectedItem == _items[_lastSelectPosition!]) {
|
||||||
newLastSelectPosition = _lastSelectPosition;
|
newLastSelectPosition = _lastSelectPosition!;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
_lastSelectPosition = newLastSelectPosition;
|
_lastSelectPosition = newLastSelectPosition;
|
||||||
|
|
||||||
_log.info("[itemStreamListItems] updateListHeight: list item changed");
|
_log.info("[itemStreamListItems] updateListHeight: list item changed");
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
WidgetsBinding.instance!.addPostFrameCallback((_) =>
|
||||||
(_listKey.currentState as MeasurableItemListState)?.updateListHeight());
|
(_listKey.currentState as MeasurableItemListState?)
|
||||||
|
?.updateListHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildItem(BuildContext context, int index) {
|
Widget _buildItem(BuildContext context, int index) {
|
||||||
|
@ -139,8 +140,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||||
if (_isRangeSelectionMode && _lastSelectPosition != null) {
|
if (_isRangeSelectionMode && _lastSelectPosition != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedItems.addAll(_items
|
_selectedItems.addAll(_items
|
||||||
.sublist(math.min(_lastSelectPosition, index),
|
.sublist(math.min(_lastSelectPosition!, index),
|
||||||
math.max(_lastSelectPosition, index) + 1)
|
math.max(_lastSelectPosition!, index) + 1)
|
||||||
.where((e) => e.isSelectable));
|
.where((e) => e.isSelectable));
|
||||||
_lastSelectPosition = index;
|
_lastSelectPosition = index;
|
||||||
});
|
});
|
||||||
|
@ -166,8 +167,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||||
if (!platform_k.isWeb && wasSelectionMode && _lastSelectPosition != null) {
|
if (!platform_k.isWeb && wasSelectionMode && _lastSelectPosition != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedItems.addAll(_items
|
_selectedItems.addAll(_items
|
||||||
.sublist(math.min(_lastSelectPosition, index),
|
.sublist(math.min(_lastSelectPosition!, index),
|
||||||
math.max(_lastSelectPosition, index) + 1)
|
math.max(_lastSelectPosition!, index) + 1)
|
||||||
.where((e) => e.isSelectable));
|
.where((e) => e.isSelectable));
|
||||||
_lastSelectPosition = index;
|
_lastSelectPosition = index;
|
||||||
});
|
});
|
||||||
|
@ -183,8 +184,8 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||||
if (!SessionStorage().hasShowRangeSelectNotification) {
|
if (!SessionStorage().hasShowRangeSelectNotification) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(platform_k.isWeb
|
content: Text(platform_k.isWeb
|
||||||
? AppLocalizations.of(context).webSelectRangeNotification
|
? AppLocalizations.of(context)!.webSelectRangeNotification
|
||||||
: AppLocalizations.of(context).mobileSelectRangeNotification),
|
: AppLocalizations.of(context)!.mobileSelectRangeNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
SessionStorage().hasShowRangeSelectNotification = true;
|
SessionStorage().hasShowRangeSelectNotification = true;
|
||||||
|
@ -205,14 +206,14 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.where((element) => element != null)
|
.whereType<SelectableItem>()
|
||||||
.toList();
|
.toList();
|
||||||
_selectedItems
|
_selectedItems
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(newSelectedItems);
|
..addAll(newSelectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
int _lastSelectPosition;
|
int? _lastSelectPosition;
|
||||||
bool _isRangeSelectionMode = false;
|
bool _isRangeSelectionMode = false;
|
||||||
|
|
||||||
var _items = <SelectableItem>[];
|
var _items = <SelectableItem>[];
|
||||||
|
|
|
@ -23,12 +23,16 @@ class SettingsArguments {
|
||||||
class Settings extends StatefulWidget {
|
class Settings extends StatefulWidget {
|
||||||
static const routeName = "/settings";
|
static const routeName = "/settings";
|
||||||
|
|
||||||
|
static Route buildRoute(SettingsArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => Settings.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
Settings({
|
Settings({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Settings.fromArgs(SettingsArguments args, {Key key})
|
Settings.fromArgs(SettingsArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
account: args.account,
|
account: args.account,
|
||||||
);
|
);
|
||||||
|
@ -43,7 +47,7 @@ class _SettingsState extends State<Settings> {
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_isEnableExif = Pref.inst().isEnableExif();
|
_isEnableExif = Pref.inst().isEnableExifOr();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -58,40 +62,41 @@ class _SettingsState extends State<Settings> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
final translator = AppLocalizations.of(context).translator;
|
final translator = AppLocalizations.of(context)!.translator;
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
pinned: true,
|
pinned: true,
|
||||||
title: Text(AppLocalizations.of(context).settingsWidgetTitle),
|
title: Text(AppLocalizations.of(context)!.settingsWidgetTitle),
|
||||||
),
|
),
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate(
|
||||||
[
|
[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(AppLocalizations.of(context).settingsLanguageTitle),
|
title:
|
||||||
|
Text(AppLocalizations.of(context)!.settingsLanguageTitle),
|
||||||
subtitle: Text(language_util.getSelectedLanguageName(context)),
|
subtitle: Text(language_util.getSelectedLanguageName(context)),
|
||||||
onTap: () => _onLanguageTap(context),
|
onTap: () => _onLanguageTap(context),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title:
|
title: Text(
|
||||||
Text(AppLocalizations.of(context).settingsExifSupportTitle),
|
AppLocalizations.of(context)!.settingsExifSupportTitle),
|
||||||
subtitle: _isEnableExif
|
subtitle: _isEnableExif
|
||||||
? Text(AppLocalizations.of(context)
|
? Text(AppLocalizations.of(context)!
|
||||||
.settingsExifSupportTrueSubtitle)
|
.settingsExifSupportTrueSubtitle)
|
||||||
: null,
|
: null,
|
||||||
value: _isEnableExif,
|
value: _isEnableExif,
|
||||||
onChanged: (value) => _onExifSupportChanged(context, value),
|
onChanged: (value) => _onExifSupportChanged(context, value),
|
||||||
),
|
),
|
||||||
_buildCaption(context,
|
_buildCaption(context,
|
||||||
AppLocalizations.of(context).settingsAboutSectionTitle),
|
AppLocalizations.of(context)!.settingsAboutSectionTitle),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(AppLocalizations.of(context).settingsVersionTitle),
|
title: Text(AppLocalizations.of(context)!.settingsVersionTitle),
|
||||||
subtitle: const Text(k.versionStr),
|
subtitle: const Text(k.versionStr),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title:
|
title:
|
||||||
Text(AppLocalizations.of(context).settingsSourceCodeTitle),
|
Text(AppLocalizations.of(context)!.settingsSourceCodeTitle),
|
||||||
subtitle: Text(_sourceRepo),
|
subtitle: Text(_sourceRepo),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await launch(_sourceRepo);
|
await launch(_sourceRepo);
|
||||||
|
@ -99,7 +104,7 @@ class _SettingsState extends State<Settings> {
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title:
|
title:
|
||||||
Text(AppLocalizations.of(context).settingsBugReportTitle),
|
Text(AppLocalizations.of(context)!.settingsBugReportTitle),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launch(_bugReportUrl);
|
launch(_bugReportUrl);
|
||||||
},
|
},
|
||||||
|
@ -107,7 +112,7 @@ class _SettingsState extends State<Settings> {
|
||||||
if (translator.isNotEmpty)
|
if (translator.isNotEmpty)
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).settingsTranslatorTitle),
|
AppLocalizations.of(context)!.settingsTranslatorTitle),
|
||||||
subtitle: Text(translator),
|
subtitle: Text(translator),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launch(_translationUrl);
|
launch(_translationUrl);
|
||||||
|
@ -172,8 +177,8 @@ class _SettingsState extends State<Settings> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).exifSupportConfirmationDialogTitle),
|
AppLocalizations.of(context)!.exifSupportConfirmationDialogTitle),
|
||||||
content: Text(AppLocalizations.of(context).exifSupportDetails),
|
content: Text(AppLocalizations.of(context)!.exifSupportDetails),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -185,7 +190,7 @@ class _SettingsState extends State<Settings> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(true);
|
Navigator.of(context).pop(true);
|
||||||
},
|
},
|
||||||
child: Text(AppLocalizations.of(context).enableButtonLabel),
|
child: Text(AppLocalizations.of(context)!.enableButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -215,7 +220,7 @@ class _SettingsState extends State<Settings> {
|
||||||
_log.severe("[_setExifSupport] Failed writing pref");
|
_log.severe("[_setExifSupport] Failed writing pref");
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).writePreferenceFailureNotification),
|
AppLocalizations.of(context)!.writePreferenceFailureNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -231,7 +236,7 @@ class _SettingsState extends State<Settings> {
|
||||||
static const String _translationUrl =
|
static const String _translationUrl =
|
||||||
"https://gitlab.com/nkming2/nc-photos/-/tree/master/lib/l10n";
|
"https://gitlab.com/nkming2/nc-photos/-/tree/master/lib/l10n";
|
||||||
|
|
||||||
bool _isEnableExif;
|
late bool _isEnableExif;
|
||||||
|
|
||||||
static final _log = Logger("widget.settings._SettingsState");
|
static final _log = Logger("widget.settings._SettingsState");
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import 'package:nc_photos/widget/sign_in.dart';
|
||||||
import 'package:page_view_indicators/circle_page_indicator.dart';
|
import 'package:page_view_indicators/circle_page_indicator.dart';
|
||||||
|
|
||||||
bool isNeedSetup() =>
|
bool isNeedSetup() =>
|
||||||
Pref.inst().getSetupProgress() & _PageId.all != _PageId.all;
|
Pref.inst().getSetupProgressOr() & _PageId.all != _PageId.all;
|
||||||
|
|
||||||
class Setup extends StatefulWidget {
|
class Setup extends StatefulWidget {
|
||||||
static const routeName = "/setup";
|
static const routeName = "/setup";
|
||||||
|
@ -29,15 +29,15 @@ class _SetupState extends State<Setup> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAppBar(BuildContext context) {
|
PreferredSizeWidget _buildAppBar(BuildContext context) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
title: Text(AppLocalizations.of(context).setupWidgetTitle),
|
title: Text(AppLocalizations.of(context)!.setupWidgetTitle),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
final page = _pageController.hasClients ? _pageController.page.round() : 0;
|
final page = _pageController.hasClients ? _pageController.page!.round() : 0;
|
||||||
final pages = <Widget>[
|
final pages = <Widget>[
|
||||||
if (_initialProgress & _PageId.exif == 0) _Exif(),
|
if (_initialProgress & _PageId.exif == 0) _Exif(),
|
||||||
if (_initialProgress & _PageId.hiddenPrefDirNotice == 0)
|
if (_initialProgress & _PageId.hiddenPrefDirNotice == 0)
|
||||||
|
@ -70,16 +70,21 @@ class _SetupState extends State<Setup> {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _onDonePressed,
|
onPressed: _onDonePressed,
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).doneButtonLabel),
|
AppLocalizations.of(context)!.doneButtonLabel),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => _onNextPressed(
|
onPressed: () {
|
||||||
(pages[_pageController.page.round()] as _Page)
|
if (_pageController.hasClients) {
|
||||||
.getPageId()),
|
_onNextPressed(
|
||||||
|
(pages[_pageController.page!.round()]
|
||||||
|
as _Page)
|
||||||
|
.getPageId());
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).nextButtonLabel),
|
AppLocalizations.of(context)!.nextButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -107,12 +112,12 @@ class _SetupState extends State<Setup> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onNextPressed(int pageId) {
|
void _onNextPressed(int pageId) {
|
||||||
Pref.inst().setSetupProgress(Pref.inst().getSetupProgress() | pageId);
|
Pref.inst().setSetupProgress(Pref.inst().getSetupProgressOr() | pageId);
|
||||||
_pageController.nextPage(
|
_pageController.nextPage(
|
||||||
duration: k.animationDurationNormal, curve: Curves.easeInOut);
|
duration: k.animationDurationNormal, curve: Curves.easeInOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _initialProgress = Pref.inst().getSetupProgress();
|
final _initialProgress = Pref.inst().getSetupProgressOr();
|
||||||
final _pageController = PageController();
|
final _pageController = PageController();
|
||||||
var _currentPageNotifier = ValueNotifier<int>(0);
|
var _currentPageNotifier = ValueNotifier<int>(0);
|
||||||
}
|
}
|
||||||
|
@ -143,23 +148,23 @@ class _ExifState extends State<_Exif> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(AppLocalizations.of(context).settingsExifSupportTitle),
|
title: Text(AppLocalizations.of(context)!.settingsExifSupportTitle),
|
||||||
value: _isEnableExif,
|
value: _isEnableExif,
|
||||||
onChanged: _onValueChanged,
|
onChanged: _onValueChanged,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(AppLocalizations.of(context).exifSupportDetails),
|
child: Text(AppLocalizations.of(context)!.exifSupportDetails),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).setupSettingsModifyLaterHint,
|
AppLocalizations.of(context)!.setupSettingsModifyLaterHint,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodyText2
|
.bodyText2!
|
||||||
.copyWith(fontStyle: FontStyle.italic)),
|
.copyWith(fontStyle: FontStyle.italic)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
@ -175,7 +180,7 @@ class _ExifState extends State<_Exif> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isEnableExif = Pref.inst().isEnableExif();
|
bool _isEnableExif = Pref.inst().isEnableExifOr();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HiddenPrefDirNotice extends StatefulWidget implements _Page {
|
class _HiddenPrefDirNotice extends StatefulWidget implements _Page {
|
||||||
|
@ -196,7 +201,7 @@ class _HiddenPrefDirNoticeState extends State<_HiddenPrefDirNotice> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).setupHiddenPrefDirNoticeDetail),
|
AppLocalizations.of(context)!.setupHiddenPrefDirNoticeDetail),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Align(
|
Align(
|
||||||
|
|
|
@ -16,7 +16,7 @@ import 'package:nc_photos/widget/root_picker.dart';
|
||||||
class SignIn extends StatefulWidget {
|
class SignIn extends StatefulWidget {
|
||||||
static const routeName = "/sign-in";
|
static const routeName = "/sign-in";
|
||||||
|
|
||||||
SignIn({Key key}) : super(key: key);
|
SignIn({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
createState() => _SignInState();
|
createState() => _SignInState();
|
||||||
|
@ -49,7 +49,7 @@ class _SignInState extends State<SignIn> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).signInHeaderText,
|
AppLocalizations.of(context)!.signInHeaderText,
|
||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -70,7 +70,7 @@ class _SignInState extends State<SignIn> {
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 32, vertical: 16),
|
horizontal: 32, vertical: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).signIn2faHintText,
|
AppLocalizations.of(context)!.signIn2faHintText,
|
||||||
style: TextStyle(fontStyle: FontStyle.italic),
|
style: TextStyle(fontStyle: FontStyle.italic),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -82,7 +82,7 @@ class _SignInState extends State<SignIn> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
if (!ModalRoute.of(context).isFirst)
|
if (!ModalRoute.of(context)!.isFirst)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
@ -94,11 +94,11 @@ class _SignInState extends State<SignIn> {
|
||||||
Container(),
|
Container(),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_formKey.currentState.validate()) {
|
if (_formKey.currentState?.validate() == true) {
|
||||||
_connect();
|
_connect();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(AppLocalizations.of(context)
|
child: Text(AppLocalizations.of(context)!
|
||||||
.connectButtonLabel),
|
.connectButtonLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -143,11 +143,11 @@ class _SignInState extends State<SignIn> {
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (newValue) {
|
onChanged: (newValue) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_scheme = newValue;
|
_scheme = newValue!;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.scheme = value.toValueString();
|
_formValue.scheme = value!.toValueString();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -159,18 +159,19 @@ class _SignInState extends State<SignIn> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(context).serverAddressInputHint,
|
hintText:
|
||||||
|
AppLocalizations.of(context)!.serverAddressInputHint,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value.trim().trimRightAny("/").isEmpty) {
|
if (value!.trim().trimRightAny("/").isEmpty) {
|
||||||
return AppLocalizations.of(context)
|
return AppLocalizations.of(context)!
|
||||||
.serverAddressInputInvalidEmpty;
|
.serverAddressInputInvalidEmpty;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.address = value.trim().trimRightAny("/");
|
_formValue.address = value!.trim().trimRightAny("/");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -179,32 +180,32 @@ class _SignInState extends State<SignIn> {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(context).usernameInputHint,
|
hintText: AppLocalizations.of(context)!.usernameInputHint,
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value.trim().isEmpty) {
|
if (value!.trim().isEmpty) {
|
||||||
return AppLocalizations.of(context).usernameInputInvalidEmpty;
|
return AppLocalizations.of(context)!.usernameInputInvalidEmpty;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.username = value;
|
_formValue.username = value!;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(context).passwordInputHint,
|
hintText: AppLocalizations.of(context)!.passwordInputHint,
|
||||||
),
|
),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value.trim().isEmpty) {
|
if (value!.trim().isEmpty) {
|
||||||
return AppLocalizations.of(context).passwordInputInvalidEmpty;
|
return AppLocalizations.of(context)!.passwordInputInvalidEmpty;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.password = value;
|
_formValue.password = value!;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -212,22 +213,23 @@ class _SignInState extends State<SignIn> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _connect() {
|
void _connect() {
|
||||||
_formKey.currentState.save();
|
_formKey.currentState!.save();
|
||||||
final account = Account(_formValue.scheme, _formValue.address,
|
final account = Account(_formValue.scheme, _formValue.address,
|
||||||
_formValue.username, _formValue.password, [""]);
|
_formValue.username, _formValue.password, [""]);
|
||||||
_log.info("[_connect] Try connecting with account: $account");
|
_log.info("[_connect] Try connecting with account: $account");
|
||||||
Navigator.pushNamed(context, Connect.routeName,
|
Navigator.pushNamed<Account>(context, Connect.routeName,
|
||||||
arguments: ConnectArguments(account))
|
arguments: ConnectArguments(account))
|
||||||
.then((result) {
|
.then<Account?>((result) {
|
||||||
return result != null
|
return result != null
|
||||||
? Navigator.pushNamed(context, RootPicker.routeName,
|
? Navigator.pushNamed(context, RootPicker.routeName,
|
||||||
arguments: RootPickerArguments(result))
|
arguments: RootPickerArguments(result))
|
||||||
: null;
|
: Future.value(null);
|
||||||
}).then((result) {
|
}).then((result) {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
// we've got a good account
|
// we've got a good account
|
||||||
// only signing in with app password would trigger distinct
|
// only signing in with app password would trigger distinct
|
||||||
final accounts = (Pref.inst().getAccounts([])..add(result)).distinct();
|
final accounts =
|
||||||
|
(Pref.inst().getAccountsOr([])..add(result)).distinct();
|
||||||
Pref.inst()
|
Pref.inst()
|
||||||
..setAccounts(accounts)
|
..setAccounts(accounts)
|
||||||
..setCurrentAccountIndex(accounts.indexOf(result));
|
..setCurrentAccountIndex(accounts.indexOf(result));
|
||||||
|
@ -267,8 +269,8 @@ extension on _Scheme {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FormValue {
|
class _FormValue {
|
||||||
String scheme;
|
late String scheme;
|
||||||
String address;
|
late String address;
|
||||||
String username;
|
late String username;
|
||||||
String password;
|
late String password;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class SimpleInputDialog extends StatefulWidget {
|
class SimpleInputDialog extends StatefulWidget {
|
||||||
SimpleInputDialog({
|
SimpleInputDialog({
|
||||||
Key key,
|
Key? key,
|
||||||
this.initialText,
|
this.initialText,
|
||||||
this.hintText,
|
this.hintText,
|
||||||
this.validator,
|
this.validator,
|
||||||
|
@ -12,9 +12,9 @@ class SimpleInputDialog extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
createState() => _SimpleInputDialogState();
|
createState() => _SimpleInputDialogState();
|
||||||
|
|
||||||
final String initialText;
|
final String? initialText;
|
||||||
final String hintText;
|
final String? hintText;
|
||||||
final FormFieldValidator<String> validator;
|
final FormFieldValidator<String>? validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SimpleInputDialogState extends State<SimpleInputDialog> {
|
class _SimpleInputDialogState extends State<SimpleInputDialog> {
|
||||||
|
@ -29,7 +29,7 @@ class _SimpleInputDialogState extends State<SimpleInputDialog> {
|
||||||
: InputDecoration(hintText: widget.hintText),
|
: InputDecoration(hintText: widget.hintText),
|
||||||
validator: widget.validator,
|
validator: widget.validator,
|
||||||
onSaved: (value) {
|
onSaved: (value) {
|
||||||
_formValue.text = value;
|
_formValue.text = value!;
|
||||||
},
|
},
|
||||||
initialValue: widget.initialText,
|
initialValue: widget.initialText,
|
||||||
),
|
),
|
||||||
|
@ -48,15 +48,15 @@ class _SimpleInputDialogState extends State<SimpleInputDialog> {
|
||||||
void _onSavePressed() {
|
void _onSavePressed() {
|
||||||
if (_formKey.currentState?.validate() == true) {
|
if (_formKey.currentState?.validate() == true) {
|
||||||
_formValue = _FormValue();
|
_formValue = _FormValue();
|
||||||
_formKey.currentState.save();
|
_formKey.currentState!.save();
|
||||||
Navigator.of(context).pop(_formValue.text);
|
Navigator.of(context).pop(_formValue.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
_FormValue _formValue;
|
var _formValue = _FormValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FormValue {
|
class _FormValue {
|
||||||
String text;
|
late String text;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import 'package:nc_photos/widget/sign_in.dart';
|
||||||
class Splash extends StatefulWidget {
|
class Splash extends StatefulWidget {
|
||||||
static const routeName = "/splash";
|
static const routeName = "/splash";
|
||||||
|
|
||||||
Splash({Key key}) : super(key: key);
|
Splash({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
createState() => _SplashState();
|
createState() => _SplashState();
|
||||||
|
@ -23,7 +23,7 @@ class _SplashState extends State<Splash> {
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
||||||
if (_shouldUpgrade()) {
|
if (_shouldUpgrade()) {
|
||||||
_handleUpgrade();
|
_handleUpgrade();
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,7 +56,7 @@ class _SplashState extends State<Splash> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).appTitle,
|
AppLocalizations.of(context)!.appTitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.headline4,
|
style: Theme.of(context).textTheme.headline4,
|
||||||
)
|
)
|
||||||
|
@ -81,12 +81,12 @@ class _SplashState extends State<Splash> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _shouldUpgrade() {
|
bool _shouldUpgrade() {
|
||||||
final lastVersion = Pref.inst().getLastVersion(k.version);
|
final lastVersion = Pref.inst().getLastVersionOr(k.version);
|
||||||
return lastVersion < k.version;
|
return lastVersion < k.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleUpgrade() {
|
void _handleUpgrade() {
|
||||||
final lastVersion = Pref.inst().getLastVersion(k.version);
|
final lastVersion = Pref.inst().getLastVersionOr(k.version);
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
final change = _gatherChangelog(lastVersion);
|
final change = _gatherChangelog(lastVersion);
|
||||||
|
@ -94,7 +94,7 @@ class _SplashState extends State<Splash> {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context).changelogTitle),
|
title: Text(AppLocalizations.of(context)!.changelogTitle),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Text(change),
|
child: Text(change),
|
||||||
),
|
),
|
||||||
|
|
|
@ -12,8 +12,8 @@ import 'package:wakelock/wakelock.dart';
|
||||||
|
|
||||||
class VideoViewer extends StatefulWidget {
|
class VideoViewer extends StatefulWidget {
|
||||||
VideoViewer({
|
VideoViewer({
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.file,
|
required this.file,
|
||||||
this.onLoaded,
|
this.onLoaded,
|
||||||
this.onHeightChanged,
|
this.onHeightChanged,
|
||||||
this.onPlay,
|
this.onPlay,
|
||||||
|
@ -27,10 +27,10 @@ class VideoViewer extends StatefulWidget {
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final File file;
|
final File file;
|
||||||
final VoidCallback onLoaded;
|
final VoidCallback? onLoaded;
|
||||||
final void Function(double height) onHeightChanged;
|
final ValueChanged<double>? onHeightChanged;
|
||||||
final VoidCallback onPlay;
|
final VoidCallback? onPlay;
|
||||||
final VoidCallback onPause;
|
final VoidCallback? onPause;
|
||||||
final bool isControlVisible;
|
final bool isControlVisible;
|
||||||
final bool canPlay;
|
final bool canPlay;
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,9 @@ class _VideoViewerState extends State<VideoViewer> {
|
||||||
)..initialize().then((_) {
|
)..initialize().then((_) {
|
||||||
widget.onLoaded?.call();
|
widget.onLoaded?.call();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
if (_key.currentContext != null) {
|
if (_key.currentContext != null) {
|
||||||
widget.onHeightChanged?.call(_key.currentContext.size.height);
|
widget.onHeightChanged?.call(_key.currentContext!.size!.height);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catchError((e, stacktrace) {
|
}).catchError((e, stacktrace) {
|
||||||
|
@ -80,13 +80,13 @@ class _VideoViewerState extends State<VideoViewer> {
|
||||||
@override
|
@override
|
||||||
dispose() {
|
dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_controller?.dispose();
|
_controller.dispose();
|
||||||
Wakelock.disable();
|
Wakelock.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlayer(BuildContext context) {
|
Widget _buildPlayer(BuildContext context) {
|
||||||
if (_controller.value.isPlaying && !widget.canPlay) {
|
if (_controller.value.isPlaying && !widget.canPlay) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
_pause();
|
_pause();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -197,7 +197,7 @@ class _VideoViewerState extends State<VideoViewer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final _key = GlobalKey();
|
final _key = GlobalKey();
|
||||||
VideoPlayerController _controller;
|
late VideoPlayerController _controller;
|
||||||
var _isFinished = false;
|
var _isFinished = false;
|
||||||
|
|
||||||
static final _log = Logger("widget.video_viewer._VideoViewerState");
|
static final _log = Logger("widget.video_viewer._VideoViewerState");
|
||||||
|
|
|
@ -38,14 +38,18 @@ class ViewerArguments {
|
||||||
class Viewer extends StatefulWidget {
|
class Viewer extends StatefulWidget {
|
||||||
static const routeName = "/viewer";
|
static const routeName = "/viewer";
|
||||||
|
|
||||||
|
static Route buildRoute(ViewerArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => Viewer.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
Viewer({
|
Viewer({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.streamFiles,
|
required this.streamFiles,
|
||||||
@required this.startIndex,
|
required this.startIndex,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Viewer.fromArgs(ViewerArguments args, {Key key})
|
Viewer.fromArgs(ViewerArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
account: args.account,
|
||||||
|
@ -124,7 +128,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
children: [
|
children: [
|
||||||
Container(color: Colors.black),
|
Container(color: Colors.black),
|
||||||
if (!_pageController.hasClients ||
|
if (!_pageController.hasClients ||
|
||||||
!_pageStates[_pageController.page.round()].hasLoaded)
|
!_pageStates[_pageController.page!.round()]!.hasLoaded)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const CircularProgressIndicator(),
|
child: const CircularProgressIndicator(),
|
||||||
|
@ -260,7 +264,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
if (!_isDetailPaneActive && _canOpenDetailPane())
|
if (!_isDetailPaneActive && _canOpenDetailPane())
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
tooltip: AppLocalizations.of(context).detailsTooltip,
|
tooltip: AppLocalizations.of(context)!.detailsTooltip,
|
||||||
onPressed: _onDetailsPressed,
|
onPressed: _onDetailsPressed,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -307,7 +311,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
Icons.share_outlined,
|
Icons.share_outlined,
|
||||||
color: Colors.white.withOpacity(.87),
|
color: Colors.white.withOpacity(.87),
|
||||||
),
|
),
|
||||||
tooltip: AppLocalizations.of(context).shareTooltip,
|
tooltip: AppLocalizations.of(context)!.shareTooltip,
|
||||||
onPressed: () => _onSharePressed(context),
|
onPressed: () => _onSharePressed(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -318,7 +322,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
Icons.download_outlined,
|
Icons.download_outlined,
|
||||||
color: Colors.white.withOpacity(.87),
|
color: Colors.white.withOpacity(.87),
|
||||||
),
|
),
|
||||||
tooltip: AppLocalizations.of(context).downloadTooltip,
|
tooltip: AppLocalizations.of(context)!.downloadTooltip,
|
||||||
onPressed: () => _onDownloadPressed(context),
|
onPressed: () => _onDownloadPressed(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -329,7 +333,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
Icons.delete_outlined,
|
Icons.delete_outlined,
|
||||||
color: Colors.white.withOpacity(.87),
|
color: Colors.white.withOpacity(.87),
|
||||||
),
|
),
|
||||||
tooltip: AppLocalizations.of(context).deleteTooltip,
|
tooltip: AppLocalizations.of(context)!.deleteTooltip,
|
||||||
onPressed: () => _onDeletePressed(context),
|
onPressed: () => _onDeletePressed(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -344,7 +348,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
Widget _buildPage(BuildContext context, int index) {
|
Widget _buildPage(BuildContext context, int index) {
|
||||||
if (_pageStates[index] == null) {
|
if (_pageStates[index] == null) {
|
||||||
_onCreateNewPage(context, index);
|
_onCreateNewPage(context, index);
|
||||||
} else if (!_pageStates[index].scrollController.hasClients) {
|
} else if (!_pageStates[index]!.scrollController.hasClients) {
|
||||||
// the page has been moved out of view and is now coming back
|
// the page has been moved out of view and is now coming back
|
||||||
_log.fine("[_buildPage] Recreating page#$index");
|
_log.fine("[_buildPage] Recreating page#$index");
|
||||||
_onRecreatePageAfterMovedOut(context, index);
|
_onRecreatePageAfterMovedOut(context, index);
|
||||||
|
@ -359,7 +363,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (notif) => _onPageContentScrolled(notif, index),
|
onNotification: (notif) => _onPageContentScrolled(notif, index),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: _pageStates[index].scrollController,
|
controller: _pageStates[index]!.scrollController,
|
||||||
physics:
|
physics:
|
||||||
_isDetailPaneActive ? null : const NeverScrollableScrollPhysics(),
|
_isDetailPaneActive ? null : const NeverScrollableScrollPhysics(),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
@ -409,7 +413,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
return _buildVideoView(context, index);
|
return _buildVideoView(context, index);
|
||||||
} else {
|
} else {
|
||||||
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}");
|
_log.shout("[_buildItemView] Unknown file format: ${file.contentType}");
|
||||||
_pageStates[index].itemHeight = 0;
|
_pageStates[index]!.itemHeight = 0;
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,7 +456,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (notification is ScrollEndNotification) {
|
if (notification is ScrollEndNotification) {
|
||||||
final scrollPos = _pageStates[index].scrollController.position;
|
final scrollPos = _pageStates[index]!.scrollController.position;
|
||||||
if (scrollPos.pixels == 0) {
|
if (scrollPos.pixels == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onDetailPaneClosed();
|
_onDetailPaneClosed();
|
||||||
|
@ -463,14 +467,15 @@ class _ViewerState extends State<Viewer> {
|
||||||
// upward, open the pane to its minimal size
|
// upward, open the pane to its minimal size
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_openDetailPane(_pageController.page.toInt(),
|
_openDetailPane(_pageController.page!.toInt(),
|
||||||
shouldAnimate: true);
|
shouldAnimate: true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (scrollPos.userScrollDirection == ScrollDirection.forward) {
|
} else if (scrollPos.userScrollDirection == ScrollDirection.forward) {
|
||||||
// downward, close the pane
|
// downward, close the pane
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
_closeDetailPane(_pageController.page.toInt(), shouldAnimate: true);
|
_closeDetailPane(_pageController.page!.toInt(),
|
||||||
|
shouldAnimate: true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -481,8 +486,8 @@ class _ViewerState extends State<Viewer> {
|
||||||
void _onImageLoaded(int index) {
|
void _onImageLoaded(int index) {
|
||||||
// currently pageview doesn't pre-load pages, we do it manually
|
// currently pageview doesn't pre-load pages, we do it manually
|
||||||
// don't pre-load if user already navigated away
|
// don't pre-load if user already navigated away
|
||||||
if (_pageController.page.round() == index &&
|
if (_pageController.page!.round() == index &&
|
||||||
!_pageStates[index].hasLoaded) {
|
!_pageStates[index]!.hasLoaded) {
|
||||||
_log.info("[_onImageLoaded] Pre-loading nearby images");
|
_log.info("[_onImageLoaded] Pre-loading nearby images");
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
final prevFile = widget.streamFiles[index - 1];
|
final prevFile = widget.streamFiles[index - 1];
|
||||||
|
@ -497,16 +502,16 @@ class _ViewerState extends State<Viewer> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_pageStates[index].hasLoaded = true;
|
_pageStates[index]!.hasLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onVideoLoaded(int index) {
|
void _onVideoLoaded(int index) {
|
||||||
if (_pageController.page.round() == index &&
|
if (_pageController.page!.round() == index &&
|
||||||
!_pageStates[index].hasLoaded) {
|
!_pageStates[index]!.hasLoaded) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pageStates[index].hasLoaded = true;
|
_pageStates[index]!.hasLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -534,16 +539,16 @@ class _ViewerState extends State<Viewer> {
|
||||||
/// Called when the page is being built after previously moved out of view
|
/// Called when the page is being built after previously moved out of view
|
||||||
void _onRecreatePageAfterMovedOut(BuildContext context, int index) {
|
void _onRecreatePageAfterMovedOut(BuildContext context, int index) {
|
||||||
if (_isShowDetailPane && !_isClosingDetailPane) {
|
if (_isShowDetailPane && !_isClosingDetailPane) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
if (_pageStates[index].itemHeight != null) {
|
if (_pageStates[index]!.itemHeight != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_openDetailPane(index);
|
_openDetailPane(index);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
_pageStates[index].scrollController.jumpTo(0);
|
_pageStates[index]!.scrollController.jumpTo(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -551,26 +556,26 @@ class _ViewerState extends State<Viewer> {
|
||||||
void _onDetailsPressed() {
|
void _onDetailsPressed() {
|
||||||
if (!_isDetailPaneActive) {
|
if (!_isDetailPaneActive) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_openDetailPane(_pageController.page.toInt(), shouldAnimate: true);
|
_openDetailPane(_pageController.page!.toInt(), shouldAnimate: true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSharePressed(BuildContext context) {
|
void _onSharePressed(BuildContext context) {
|
||||||
assert(platform_k.isAndroid);
|
assert(platform_k.isAndroid);
|
||||||
final file = widget.streamFiles[_pageController.page.round()];
|
final file = widget.streamFiles[_pageController.page!.round()];
|
||||||
ShareHandler().shareFiles(context, widget.account, [file]);
|
ShareHandler().shareFiles(context, widget.account, [file]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownloadPressed(BuildContext context) async {
|
void _onDownloadPressed(BuildContext context) async {
|
||||||
final file = widget.streamFiles[_pageController.page.round()];
|
final file = widget.streamFiles[_pageController.page!.round()];
|
||||||
_log.info("[_onDownloadPressed] Downloading file: ${file.path}");
|
_log.info("[_onDownloadPressed] Downloading file: ${file.path}");
|
||||||
var controller = SnackBarManager().showSnackBar(SnackBar(
|
var controller = SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content:
|
||||||
Text(AppLocalizations.of(context).downloadProcessingNotification),
|
Text(AppLocalizations.of(context)!.downloadProcessingNotification),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
controller?.closed?.whenComplete(() {
|
controller?.closed.whenComplete(() {
|
||||||
controller = null;
|
controller = null;
|
||||||
});
|
});
|
||||||
dynamic result;
|
dynamic result;
|
||||||
|
@ -581,7 +586,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
_log.warning("[_onDownloadPressed] Permission not granted");
|
_log.warning("[_onDownloadPressed] Permission not granted");
|
||||||
controller?.close();
|
controller?.close();
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.downloadFailureNoPermissionNotification),
|
.downloadFailureNoPermissionNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -591,9 +596,9 @@ class _ViewerState extends State<Viewer> {
|
||||||
"[_onDownloadPressed] Failed while downloadFile", e, stacktrace);
|
"[_onDownloadPressed] Failed while downloadFile", e, stacktrace);
|
||||||
controller?.close();
|
controller?.close();
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content: Text(
|
||||||
Text("${AppLocalizations.of(context).downloadFailureNotification}: "
|
"${AppLocalizations.of(context)!.downloadFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
|
@ -622,19 +627,19 @@ class _ViewerState extends State<Viewer> {
|
||||||
|
|
||||||
// fallback
|
// fallback
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).downloadSuccessNotification),
|
content: Text(AppLocalizations.of(context)!.downloadSuccessNotification),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDeletePressed(BuildContext context) async {
|
void _onDeletePressed(BuildContext context) async {
|
||||||
final file = widget.streamFiles[_pageController.page.round()];
|
final file = widget.streamFiles[_pageController.page!.round()];
|
||||||
_log.info("[_onDeletePressed] Removing file: ${file.path}");
|
_log.info("[_onDeletePressed] Removing file: ${file.path}");
|
||||||
var controller = SnackBarManager().showSnackBar(SnackBar(
|
var controller = SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).deleteProcessingNotification),
|
content: Text(AppLocalizations.of(context)!.deleteProcessingNotification),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
controller?.closed?.whenComplete(() {
|
controller?.closed.whenComplete(() {
|
||||||
controller = null;
|
controller = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
@ -642,7 +647,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
AlbumRepo(AlbumCachedDataSource()))(widget.account, file);
|
AlbumRepo(AlbumCachedDataSource()))(widget.account, file);
|
||||||
controller?.close();
|
controller?.close();
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).deleteSuccessNotification),
|
content: Text(AppLocalizations.of(context)!.deleteSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -655,7 +660,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
controller?.close();
|
controller?.close();
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content:
|
||||||
Text("${AppLocalizations.of(context).deleteFailureNotification}: "
|
Text("${AppLocalizations.of(context)!.deleteFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -666,8 +671,9 @@ class _ViewerState extends State<Viewer> {
|
||||||
if (_pageStates[index]?.itemHeight == null) {
|
if (_pageStates[index]?.itemHeight == null) {
|
||||||
return MediaQuery.of(context).size.height;
|
return MediaQuery.of(context).size.height;
|
||||||
} else {
|
} else {
|
||||||
return _pageStates[index].itemHeight +
|
return _pageStates[index]!.itemHeight! +
|
||||||
(MediaQuery.of(context).size.height - _pageStates[index].itemHeight) /
|
(MediaQuery.of(context).size.height -
|
||||||
|
_pageStates[index]!.itemHeight!) /
|
||||||
2 -
|
2 -
|
||||||
4;
|
4;
|
||||||
}
|
}
|
||||||
|
@ -680,10 +686,10 @@ class _ViewerState extends State<Viewer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateItemHeight(int index, double height) {
|
void _updateItemHeight(int index, double height) {
|
||||||
if (_pageStates[index].itemHeight != height) {
|
if (_pageStates[index]!.itemHeight != height) {
|
||||||
_log.fine("[_updateItemHeight] New height of item#$index: $height");
|
_log.fine("[_updateItemHeight] New height of item#$index: $height");
|
||||||
setState(() {
|
setState(() {
|
||||||
_pageStates[index].itemHeight = height;
|
_pageStates[index]!.itemHeight = height;
|
||||||
if (_isDetailPaneActive) {
|
if (_isDetailPaneActive) {
|
||||||
_openDetailPane(index);
|
_openDetailPane(index);
|
||||||
}
|
}
|
||||||
|
@ -709,12 +715,12 @@ class _ViewerState extends State<Viewer> {
|
||||||
_isShowDetailPane = true;
|
_isShowDetailPane = true;
|
||||||
_isDetailPaneActive = true;
|
_isDetailPaneActive = true;
|
||||||
if (shouldAnimate) {
|
if (shouldAnimate) {
|
||||||
_pageStates[index].scrollController.animateTo(
|
_pageStates[index]!.scrollController.animateTo(
|
||||||
_calcDetailPaneOpenedScrollPosition(index),
|
_calcDetailPaneOpenedScrollPosition(index),
|
||||||
duration: k.animationDurationNormal,
|
duration: k.animationDurationNormal,
|
||||||
curve: Curves.easeOut);
|
curve: Curves.easeOut);
|
||||||
} else {
|
} else {
|
||||||
_pageStates[index]
|
_pageStates[index]!
|
||||||
.scrollController
|
.scrollController
|
||||||
.jumpTo(_calcDetailPaneOpenedScrollPosition(index));
|
.jumpTo(_calcDetailPaneOpenedScrollPosition(index));
|
||||||
}
|
}
|
||||||
|
@ -723,7 +729,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
void _closeDetailPane(int index, {bool shouldAnimate = false}) {
|
void _closeDetailPane(int index, {bool shouldAnimate = false}) {
|
||||||
_isClosingDetailPane = true;
|
_isClosingDetailPane = true;
|
||||||
if (shouldAnimate) {
|
if (shouldAnimate) {
|
||||||
_pageStates[index].scrollController.animateTo(0,
|
_pageStates[index]!.scrollController.animateTo(0,
|
||||||
duration: k.animationDurationNormal, curve: Curves.easeOut);
|
duration: k.animationDurationNormal, curve: Curves.easeOut);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -739,7 +745,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
.previousPage(
|
.previousPage(
|
||||||
duration: k.animationDurationNormal, curve: Curves.easeInOut)
|
duration: k.animationDurationNormal, curve: Curves.easeInOut)
|
||||||
.whenComplete(
|
.whenComplete(
|
||||||
() => _updateNavigationState(_pageController.page.round()));
|
() => _updateNavigationState(_pageController.page!.round()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to the next image in the stream
|
/// Switch to the next image in the stream
|
||||||
|
@ -747,7 +753,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
_pageController
|
_pageController
|
||||||
.nextPage(duration: k.animationDurationNormal, curve: Curves.easeInOut)
|
.nextPage(duration: k.animationDurationNormal, curve: Curves.easeInOut)
|
||||||
.whenComplete(
|
.whenComplete(
|
||||||
() => _updateNavigationState(_pageController.page.round()));
|
() => _updateNavigationState(_pageController.page!.round()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to the image on the "left", what that means depend on the current
|
/// Switch to the image on the "left", what that means depend on the current
|
||||||
|
@ -819,7 +825,7 @@ class _ViewerState extends State<Viewer> {
|
||||||
|
|
||||||
var _isZoomed = false;
|
var _isZoomed = false;
|
||||||
|
|
||||||
PageController _pageController;
|
late PageController _pageController;
|
||||||
final _pageStates = <int, _PageState>{};
|
final _pageStates = <int, _PageState>{};
|
||||||
|
|
||||||
/// used to gain focus on web for keyboard support
|
/// used to gain focus on web for keyboard support
|
||||||
|
@ -832,6 +838,6 @@ class _PageState {
|
||||||
_PageState(this.scrollController);
|
_PageState(this.scrollController);
|
||||||
|
|
||||||
ScrollController scrollController;
|
ScrollController scrollController;
|
||||||
double itemHeight;
|
double? itemHeight;
|
||||||
bool hasLoaded = false;
|
bool hasLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,9 +32,9 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class ViewerDetailPane extends StatefulWidget {
|
class ViewerDetailPane extends StatefulWidget {
|
||||||
const ViewerDetailPane({
|
const ViewerDetailPane({
|
||||||
Key key,
|
Key? key,
|
||||||
@required this.account,
|
required this.account,
|
||||||
@required this.file,
|
required this.file,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -54,7 +54,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
_log.info("[initState] Metadata missing in File");
|
_log.info("[initState] Metadata missing in File");
|
||||||
} else {
|
} else {
|
||||||
_log.info("[initState] Metadata exists in File");
|
_log.info("[initState] Metadata exists in File");
|
||||||
if (widget.file.metadata.exif != null) {
|
if (widget.file.metadata!.exif != null) {
|
||||||
_initMetadata();
|
_initMetadata();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,29 +73,29 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
const space = " ";
|
const space = " ";
|
||||||
if (widget.file.metadata?.imageWidth != null &&
|
if (widget.file.metadata?.imageWidth != null &&
|
||||||
widget.file.metadata?.imageHeight != null) {
|
widget.file.metadata?.imageHeight != null) {
|
||||||
final pixelCount =
|
final pixelCount = widget.file.metadata!.imageWidth! *
|
||||||
widget.file.metadata.imageWidth * widget.file.metadata.imageHeight;
|
widget.file.metadata!.imageHeight!;
|
||||||
if (pixelCount >= 500000) {
|
if (pixelCount >= 500000) {
|
||||||
final mpCount = pixelCount / 1000000.0;
|
final mpCount = pixelCount / 1000000.0;
|
||||||
sizeSubStr += AppLocalizations.of(context)
|
sizeSubStr += AppLocalizations.of(context)!
|
||||||
.megapixelCount(mpCount.toStringAsFixed(1));
|
.megapixelCount(mpCount.toStringAsFixed(1));
|
||||||
sizeSubStr += space;
|
sizeSubStr += space;
|
||||||
}
|
}
|
||||||
sizeSubStr += _byteSizeToString(widget.file.contentLength);
|
sizeSubStr += _byteSizeToString(widget.file.contentLength ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
String cameraSubStr = "";
|
String cameraSubStr = "";
|
||||||
if (_fNumber != null) {
|
if (_fNumber != null) {
|
||||||
cameraSubStr += "f/${_fNumber.toStringAsFixed(1)}$space";
|
cameraSubStr += "f/${_fNumber!.toStringAsFixed(1)}$space";
|
||||||
}
|
}
|
||||||
if (_exposureTime != null) {
|
if (_exposureTime != null) {
|
||||||
cameraSubStr +=
|
cameraSubStr +=
|
||||||
AppLocalizations.of(context).secondCountSymbol(_exposureTime);
|
AppLocalizations.of(context)!.secondCountSymbol(_exposureTime!);
|
||||||
cameraSubStr += space;
|
cameraSubStr += space;
|
||||||
}
|
}
|
||||||
if (_focalLength != null) {
|
if (_focalLength != null) {
|
||||||
cameraSubStr += AppLocalizations.of(context)
|
cameraSubStr += AppLocalizations.of(context)!
|
||||||
.millimeterCountSymbol(_focalLength.toStringAsFixedTruncated(2));
|
.millimeterCountSymbol(_focalLength!.toStringAsFixedTruncated(2));
|
||||||
cameraSubStr += space;
|
cameraSubStr += space;
|
||||||
}
|
}
|
||||||
if (_isoSpeedRatings != null) {
|
if (_isoSpeedRatings != null) {
|
||||||
|
@ -112,12 +112,12 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
children: [
|
children: [
|
||||||
_DetailPaneButton(
|
_DetailPaneButton(
|
||||||
icon: Icons.playlist_add_outlined,
|
icon: Icons.playlist_add_outlined,
|
||||||
label: AppLocalizations.of(context).addToAlbumTooltip,
|
label: AppLocalizations.of(context)!.addToAlbumTooltip,
|
||||||
onPressed: () => _onAddToAlbumPressed(context),
|
onPressed: () => _onAddToAlbumPressed(context),
|
||||||
),
|
),
|
||||||
_DetailPaneButton(
|
_DetailPaneButton(
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
label: AppLocalizations.of(context).deleteTooltip,
|
label: AppLocalizations.of(context)!.deleteTooltip,
|
||||||
onPressed: () => _onDeletePressed(context),
|
onPressed: () => _onDeletePressed(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -154,7 +154,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
color: AppTheme.getSecondaryTextColor(context),
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
"${widget.file.metadata.imageWidth} x ${widget.file.metadata.imageHeight}"),
|
"${widget.file.metadata!.imageWidth} x ${widget.file.metadata!.imageHeight}"),
|
||||||
subtitle: Text(sizeSubStr),
|
subtitle: Text(sizeSubStr),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
@ -163,7 +163,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
Icons.aspect_ratio,
|
Icons.aspect_ratio,
|
||||||
color: AppTheme.getSecondaryTextColor(context),
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
),
|
),
|
||||||
title: Text(_byteSizeToString(widget.file.contentLength)),
|
title: Text(_byteSizeToString(widget.file.contentLength ?? 0)),
|
||||||
),
|
),
|
||||||
if (_model != null)
|
if (_model != null)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -171,14 +171,14 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
Icons.camera_outlined,
|
Icons.camera_outlined,
|
||||||
color: AppTheme.getSecondaryTextColor(context),
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
),
|
),
|
||||||
title: Text(_model),
|
title: Text(_model!),
|
||||||
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
|
subtitle: cameraSubStr.isNotEmpty ? Text(cameraSubStr) : null,
|
||||||
),
|
),
|
||||||
if (features.isSupportMapView && _gps != null)
|
if (features.isSupportMapView && _gps != null)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 256,
|
height: 256,
|
||||||
child: platform.Map(
|
child: platform.Map(
|
||||||
center: _gps,
|
center: _gps!,
|
||||||
zoom: 16,
|
zoom: 16,
|
||||||
onTap: _onMapTap,
|
onTap: _onMapTap,
|
||||||
),
|
),
|
||||||
|
@ -190,35 +190,35 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
|
|
||||||
/// Convert EXIF data to readable format
|
/// Convert EXIF data to readable format
|
||||||
void _initMetadata() {
|
void _initMetadata() {
|
||||||
final exif = widget.file.metadata.exif;
|
final exif = widget.file.metadata!.exif!;
|
||||||
_log.info("[_initMetadata] $exif");
|
_log.info("[_initMetadata] $exif");
|
||||||
|
|
||||||
if (exif.make != null && exif.model != null) {
|
if (exif.make != null && exif.model != null) {
|
||||||
_model = "${exif.make} ${exif.model}";
|
_model = "${exif.make} ${exif.model}";
|
||||||
}
|
}
|
||||||
if (exif.fNumber != null) {
|
if (exif.fNumber != null) {
|
||||||
_fNumber = exif.fNumber.toDouble();
|
_fNumber = exif.fNumber!.toDouble();
|
||||||
}
|
}
|
||||||
if (exif.exposureTime != null) {
|
if (exif.exposureTime != null) {
|
||||||
if (exif.exposureTime.denominator == 1) {
|
if (exif.exposureTime!.denominator == 1) {
|
||||||
_exposureTime = exif.exposureTime.numerator.toString();
|
_exposureTime = exif.exposureTime!.numerator.toString();
|
||||||
} else {
|
} else {
|
||||||
_exposureTime = exif.exposureTime.toString();
|
_exposureTime = exif.exposureTime.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exif.focalLength != null) {
|
if (exif.focalLength != null) {
|
||||||
_focalLength = exif.focalLength.toDouble();
|
_focalLength = exif.focalLength!.toDouble();
|
||||||
}
|
}
|
||||||
if (exif.isoSpeedRatings != null) {
|
if (exif.isoSpeedRatings != null) {
|
||||||
_isoSpeedRatings = exif.isoSpeedRatings;
|
_isoSpeedRatings = exif.isoSpeedRatings!;
|
||||||
}
|
}
|
||||||
if (exif.gpsLatitudeRef != null &&
|
if (exif.gpsLatitudeRef != null &&
|
||||||
exif.gpsLatitude != null &&
|
exif.gpsLatitude != null &&
|
||||||
exif.gpsLongitudeRef != null &&
|
exif.gpsLongitudeRef != null &&
|
||||||
exif.gpsLongitude != null) {
|
exif.gpsLongitude != null) {
|
||||||
final lat = _gpsDmsToDouble(exif.gpsLatitude) *
|
final lat = _gpsDmsToDouble(exif.gpsLatitude!) *
|
||||||
(exif.gpsLatitudeRef == "S" ? -1 : 1);
|
(exif.gpsLatitudeRef == "S" ? -1 : 1);
|
||||||
final lng = _gpsDmsToDouble(exif.gpsLongitude) *
|
final lng = _gpsDmsToDouble(exif.gpsLongitude!) *
|
||||||
(exif.gpsLongitudeRef == "W" ? -1 : 1);
|
(exif.gpsLongitudeRef == "W" ? -1 : 1);
|
||||||
_log.fine("GPS: ($lat, $lng)");
|
_log.fine("GPS: ($lat, $lng)");
|
||||||
_gps = Tuple2(lat, lng);
|
_gps = Tuple2(lat, lng);
|
||||||
|
@ -238,7 +238,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
_log.info("[_onAddToAlbumPressed] Album picked: ${value.name}");
|
_log.info("[_onAddToAlbumPressed] Album picked: ${value.name}");
|
||||||
_addToAlbum(context, value).then((_) {
|
_addToAlbum(context, value).then((_) {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(AppLocalizations.of(context)!
|
||||||
.addToAlbumSuccessNotification(value.name)),
|
.addToAlbumSuccessNotification(value.name)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -246,7 +246,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
} else {
|
} else {
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content:
|
||||||
Text(AppLocalizations.of(context).addToAlbumFailureNotification),
|
Text(AppLocalizations.of(context)!.addToAlbumFailureNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -255,7 +255,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
"[_onAddToAlbumPressed] Failed while showDialog", e, stacktrace);
|
"[_onAddToAlbumPressed] Failed while showDialog", e, stacktrace);
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"${AppLocalizations.of(context).addToAlbumFailureNotification}: "
|
"${AppLocalizations.of(context)!.addToAlbumFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -265,10 +265,10 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
void _onDeletePressed(BuildContext context) async {
|
void _onDeletePressed(BuildContext context) async {
|
||||||
_log.info("[_onDeletePressed] Removing file: ${widget.file.path}");
|
_log.info("[_onDeletePressed] Removing file: ${widget.file.path}");
|
||||||
var controller = SnackBarManager().showSnackBar(SnackBar(
|
var controller = SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).deleteProcessingNotification),
|
content: Text(AppLocalizations.of(context)!.deleteProcessingNotification),
|
||||||
duration: k.snackBarDurationShort,
|
duration: k.snackBarDurationShort,
|
||||||
));
|
));
|
||||||
controller?.closed?.whenComplete(() {
|
controller?.closed.whenComplete(() {
|
||||||
controller = null;
|
controller = null;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
@ -276,7 +276,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
AlbumRepo(AlbumCachedDataSource()))(widget.account, widget.file);
|
AlbumRepo(AlbumCachedDataSource()))(widget.account, widget.file);
|
||||||
controller?.close();
|
controller?.close();
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).deleteSuccessNotification),
|
content: Text(AppLocalizations.of(context)!.deleteSuccessNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -289,7 +289,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
controller?.close();
|
controller?.close();
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content:
|
content:
|
||||||
Text("${AppLocalizations.of(context).deleteFailureNotification}: "
|
Text("${AppLocalizations.of(context)!.deleteFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -300,7 +300,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
if (platform_k.isAndroid) {
|
if (platform_k.isAndroid) {
|
||||||
final intent = AndroidIntent(
|
final intent = AndroidIntent(
|
||||||
action: "action_view",
|
action: "action_view",
|
||||||
data: Uri.encodeFull("geo:${_gps.item1},${_gps.item2}?z=16"),
|
data: Uri.encodeFull("geo:${_gps!.item1},${_gps!.item2}?z=16"),
|
||||||
);
|
);
|
||||||
intent.launch();
|
intent.launch();
|
||||||
}
|
}
|
||||||
|
@ -329,7 +329,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
stacktrace);
|
stacktrace);
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
AppLocalizations.of(context).updateDateTimeFailureNotification),
|
AppLocalizations.of(context)!.updateDateTimeFailureNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -362,7 +362,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
_log.info("[_addToAlbum] File already in album: ${widget.file.path}");
|
_log.info("[_addToAlbum] File already in album: ${widget.file.path}");
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"${AppLocalizations.of(context).addToAlbumAlreadyAddedNotification}"),
|
"${AppLocalizations.of(context)!.addToAlbumAlreadyAddedNotification}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
return Future.error(ArgumentError("File already in album"));
|
return Future.error(ArgumentError("File already in album"));
|
||||||
|
@ -381,7 +381,7 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
_log.shout("[_addToAlbum] Failed while updating album", e, stacktrace);
|
_log.shout("[_addToAlbum] Failed while updating album", e, stacktrace);
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"${AppLocalizations.of(context).addToAlbumFailureNotification}: "
|
"${AppLocalizations.of(context)!.addToAlbumFailureNotification}: "
|
||||||
"${exception_util.toUserString(e, context)}"),
|
"${exception_util.toUserString(e, context)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
|
@ -389,22 +389,26 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime _dateTime;
|
late DateTime _dateTime;
|
||||||
// EXIF data
|
// EXIF data
|
||||||
String _model;
|
String? _model;
|
||||||
double _fNumber;
|
double? _fNumber;
|
||||||
String _exposureTime;
|
String? _exposureTime;
|
||||||
double _focalLength;
|
double? _focalLength;
|
||||||
int _isoSpeedRatings;
|
int? _isoSpeedRatings;
|
||||||
Tuple2<double, double> _gps;
|
Tuple2<double, double>? _gps;
|
||||||
|
|
||||||
static final _log =
|
static final _log =
|
||||||
Logger("widget.viewer_detail_pane._ViewerDetailPaneState");
|
Logger("widget.viewer_detail_pane._ViewerDetailPaneState");
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailPaneButton extends StatelessWidget {
|
class _DetailPaneButton extends StatelessWidget {
|
||||||
const _DetailPaneButton({Key key, this.icon, this.label, this.onPressed})
|
const _DetailPaneButton({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
|
@ -439,7 +443,7 @@ class _DetailPaneButton extends StatelessWidget {
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback? onPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _byteSizeToString(int byteSize) {
|
String _byteSizeToString(int byteSize) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
version: 1.22.0+2200
|
version: 1.22.0+2200
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.10.0 <3.0.0"
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
@ -14,6 +14,7 @@ void main() {
|
||||||
final json = <String, dynamic>{
|
final json = <String, dynamic>{
|
||||||
"version": Album.version,
|
"version": Album.version,
|
||||||
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"name": "",
|
||||||
"provider": <String, dynamic>{
|
"provider": <String, dynamic>{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"content": <String, dynamic>{
|
"content": <String, dynamic>{
|
||||||
|
@ -30,7 +31,8 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Album.fromJson(json),
|
Album.fromJson(json,
|
||||||
|
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||||
Album(
|
Album(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -63,7 +65,8 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Album.fromJson(json),
|
Album.fromJson(json,
|
||||||
|
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||||
Album(
|
Album(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
name: "album",
|
name: "album",
|
||||||
|
@ -114,7 +117,8 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Album.fromJson(json),
|
Album.fromJson(json,
|
||||||
|
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||||
Album(
|
Album(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -161,7 +165,8 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Album.fromJson(json),
|
Album.fromJson(json,
|
||||||
|
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||||
Album(
|
Album(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -203,7 +208,8 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Album.fromJson(json),
|
Album.fromJson(json,
|
||||||
|
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||||
Album(
|
Album(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -242,7 +248,8 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Album.fromJson(json),
|
Album.fromJson(json,
|
||||||
|
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||||
Album(
|
Album(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -260,6 +267,7 @@ void main() {
|
||||||
final json = <String, dynamic>{
|
final json = <String, dynamic>{
|
||||||
"version": Album.version,
|
"version": Album.version,
|
||||||
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
|
"name": "",
|
||||||
"provider": <String, dynamic>{
|
"provider": <String, dynamic>{
|
||||||
"type": "static",
|
"type": "static",
|
||||||
"content": <String, dynamic>{
|
"content": <String, dynamic>{
|
||||||
|
@ -279,7 +287,8 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Album.fromJson(json),
|
Album.fromJson(json,
|
||||||
|
upgraderV1: null, upgraderV2: null, upgraderV3: null),
|
||||||
Album(
|
Album(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
name: "",
|
name: "",
|
||||||
|
|
|
@ -109,7 +109,7 @@ void main() {
|
||||||
"version": Metadata.version,
|
"version": Metadata.version,
|
||||||
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
"lastUpdated": "2020-01-02T03:04:05.678901Z",
|
||||||
};
|
};
|
||||||
expect(Metadata.fromJson(json),
|
expect(Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||||
Metadata(lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901)));
|
Metadata(lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ void main() {
|
||||||
"fileEtag": "8a3e0799b6f0711c23cc2d93950eceb5",
|
"fileEtag": "8a3e0799b6f0711c23cc2d93950eceb5",
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Metadata.fromJson(json),
|
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||||
Metadata(
|
Metadata(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
|
fileEtag: "8a3e0799b6f0711c23cc2d93950eceb5",
|
||||||
|
@ -134,7 +134,7 @@ void main() {
|
||||||
"imageWidth": 1024,
|
"imageWidth": 1024,
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Metadata.fromJson(json),
|
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||||
Metadata(
|
Metadata(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
imageWidth: 1024,
|
imageWidth: 1024,
|
||||||
|
@ -148,7 +148,7 @@ void main() {
|
||||||
"imageHeight": 768,
|
"imageHeight": 768,
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Metadata.fromJson(json),
|
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||||
Metadata(
|
Metadata(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
imageHeight: 768,
|
imageHeight: 768,
|
||||||
|
@ -164,7 +164,7 @@ void main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
Metadata.fromJson(json),
|
Metadata.fromJson(json, upgraderV1: null, upgraderV2: null),
|
||||||
Metadata(
|
Metadata(
|
||||||
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
|
||||||
exif: Exif({
|
exif: Exif({
|
||||||
|
|
Loading…
Reference in a new issue