mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 14:56:20 +01:00
Manage shares
This commit is contained in:
parent
3b6c679f04
commit
85daba9786
13 changed files with 965 additions and 13 deletions
202
lib/bloc/list_sharing.dart
Normal file
202
lib/bloc/list_sharing.dart
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:kiwi/kiwi.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
import 'package:nc_photos/entity/share.dart';
|
||||||
|
import 'package:nc_photos/entity/share/data_source.dart';
|
||||||
|
import 'package:nc_photos/event/event.dart';
|
||||||
|
import 'package:nc_photos/use_case/find_file.dart';
|
||||||
|
|
||||||
|
class ListSharingItem {
|
||||||
|
ListSharingItem(this.share, this.file);
|
||||||
|
|
||||||
|
final Share share;
|
||||||
|
final File file;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ListSharingBlocEvent {
|
||||||
|
const ListSharingBlocEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSharingBlocQuery extends ListSharingBlocEvent {
|
||||||
|
const ListSharingBlocQuery(this.account);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"account: $account, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ListSharingBlocShareRemoved extends ListSharingBlocEvent {
|
||||||
|
const _ListSharingBlocShareRemoved(this.share);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"share: $share, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final Share share;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ListSharingBlocState {
|
||||||
|
const ListSharingBlocState(this.account, this.items);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"account: $account, "
|
||||||
|
"items: List {length: ${items.length}}, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final Account? account;
|
||||||
|
final List<ListSharingItem> items;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSharingBlocInit extends ListSharingBlocState {
|
||||||
|
ListSharingBlocInit() : super(null, const []);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSharingBlocLoading extends ListSharingBlocState {
|
||||||
|
const ListSharingBlocLoading(Account? account, List<ListSharingItem> items)
|
||||||
|
: super(account, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSharingBlocSuccess extends ListSharingBlocState {
|
||||||
|
const ListSharingBlocSuccess(Account? account, List<ListSharingItem> items)
|
||||||
|
: super(account, items);
|
||||||
|
|
||||||
|
ListSharingBlocSuccess copyWith({
|
||||||
|
Account? account,
|
||||||
|
List<ListSharingItem>? items,
|
||||||
|
}) =>
|
||||||
|
ListSharingBlocSuccess(
|
||||||
|
account ?? this.account,
|
||||||
|
items ?? this.items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListSharingBlocFailure extends ListSharingBlocState {
|
||||||
|
const ListSharingBlocFailure(
|
||||||
|
Account? account, List<ListSharingItem> items, this.exception)
|
||||||
|
: super(account, items);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"super: ${super.toString()}, "
|
||||||
|
"exception: $exception, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
ListSharingBlocFailure copyWith({
|
||||||
|
Account? account,
|
||||||
|
List<ListSharingItem>? items,
|
||||||
|
dynamic exception,
|
||||||
|
}) =>
|
||||||
|
ListSharingBlocFailure(
|
||||||
|
account ?? this.account,
|
||||||
|
items ?? this.items,
|
||||||
|
exception ?? this.exception,
|
||||||
|
);
|
||||||
|
|
||||||
|
final dynamic exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all shares from a given file
|
||||||
|
class ListSharingBloc extends Bloc<ListSharingBlocEvent, ListSharingBlocState> {
|
||||||
|
ListSharingBloc() : super(ListSharingBlocInit()) {
|
||||||
|
_shareRemovedListener.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ListSharingBloc of(Account account) {
|
||||||
|
final id =
|
||||||
|
"${account.scheme}://${account.username}@${account.address}?${account.roots.join('&')}";
|
||||||
|
try {
|
||||||
|
_log.fine("[of] Resolving bloc for '$id'");
|
||||||
|
return KiwiContainer().resolve<ListSharingBloc>("ListSharingBloc($id)");
|
||||||
|
} catch (_) {
|
||||||
|
// no created instance for this account, make a new one
|
||||||
|
_log.info("[of] New bloc instance for account: $account");
|
||||||
|
final bloc = ListSharingBloc();
|
||||||
|
KiwiContainer().registerInstance<ListSharingBloc>(bloc,
|
||||||
|
name: "ListSharingBloc($id)");
|
||||||
|
return bloc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
mapEventToState(ListSharingBlocEvent event) async* {
|
||||||
|
_log.info("[mapEventToState] $event");
|
||||||
|
if (event is ListSharingBlocQuery) {
|
||||||
|
yield* _onEventQuery(event);
|
||||||
|
} else if (event is _ListSharingBlocShareRemoved) {
|
||||||
|
yield* _onEventShareRemoved(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<ListSharingBlocState> _onEventQuery(ListSharingBlocQuery ev) async* {
|
||||||
|
try {
|
||||||
|
yield ListSharingBlocLoading(ev.account, state.items);
|
||||||
|
yield ListSharingBlocSuccess(ev.account, await _query(ev));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe("[_onEventQuery] Exception while request", e, stackTrace);
|
||||||
|
yield ListSharingBlocFailure(ev.account, state.items, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<ListSharingBlocState> _onEventShareRemoved(
|
||||||
|
_ListSharingBlocShareRemoved ev) async* {
|
||||||
|
if (state is! ListSharingBlocSuccess && state is! ListSharingBlocFailure) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final newItems = List.of(state.items)
|
||||||
|
.where((element) => !identical(element.share, ev.share))
|
||||||
|
.toList();
|
||||||
|
// i love hacks :)
|
||||||
|
yield (state as dynamic).copyWith(
|
||||||
|
items: newItems,
|
||||||
|
) as ListSharingBlocState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onShareRemovedEvent(ShareRemovedEvent ev) {
|
||||||
|
add(_ListSharingBlocShareRemoved(ev.share));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ListSharingItem>> _query(ListSharingBlocQuery ev) async {
|
||||||
|
final shareRepo = ShareRepo(ShareRemoteDataSource());
|
||||||
|
final shares = await shareRepo.listAll(ev.account);
|
||||||
|
final futures = shares.map((e) async {
|
||||||
|
if (!file_util.isSupportedMime(e.mimeType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (ev.account.roots
|
||||||
|
.every((r) => r.isNotEmpty && !e.path.startsWith("/$r/"))) {
|
||||||
|
// ignore files not under root dirs
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final file = await FindFile()(ev.account, e.itemSource);
|
||||||
|
return ListSharingItem(e, file);
|
||||||
|
} catch (_) {
|
||||||
|
_log.warning("[_query] File not found: ${e.itemSource}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (await Future.wait(futures)).whereType<ListSharingItem>().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _shareRemovedListener =
|
||||||
|
AppEventListener<ShareRemovedEvent>(_onShareRemovedEvent);
|
||||||
|
|
||||||
|
static final _log = Logger("bloc.list_share.ListSharingBloc");
|
||||||
|
}
|
|
@ -3,8 +3,9 @@ import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
|
|
||||||
bool isSupportedFormat(File file) =>
|
bool isSupportedMime(String mime) => _supportedFormatMimes.contains(mime);
|
||||||
_supportedFormatMimes.contains(file.contentType);
|
|
||||||
|
bool isSupportedFormat(File file) => isSupportedMime(file.contentType ?? "");
|
||||||
|
|
||||||
bool isSupportedImageFormat(File file) =>
|
bool isSupportedImageFormat(File file) =>
|
||||||
isSupportedFormat(file) && file.contentType?.startsWith("image/") == true;
|
isSupportedFormat(file) && file.contentType?.startsWith("image/") == true;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:path/path.dart' as path_util;
|
||||||
|
|
||||||
enum ShareType {
|
enum ShareType {
|
||||||
user,
|
user,
|
||||||
|
@ -54,11 +55,42 @@ extension ShareTypeExtension on ShareType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ShareItemType {
|
||||||
|
file,
|
||||||
|
folder,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ShareItemTypeExtension on ShareItemType {
|
||||||
|
static ShareItemType fromValue(String itemTypeVal) {
|
||||||
|
switch (itemTypeVal) {
|
||||||
|
case "file":
|
||||||
|
return ShareItemType.file;
|
||||||
|
case "folder":
|
||||||
|
return ShareItemType.folder;
|
||||||
|
default:
|
||||||
|
throw ArgumentError("Invalid itemType: $itemTypeVal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String toValue() {
|
||||||
|
switch (this) {
|
||||||
|
case ShareItemType.file:
|
||||||
|
return "file";
|
||||||
|
case ShareItemType.folder:
|
||||||
|
return "folder";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Share with EquatableMixin {
|
class Share with EquatableMixin {
|
||||||
Share({
|
Share({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.path,
|
|
||||||
required this.shareType,
|
required this.shareType,
|
||||||
|
required this.stime,
|
||||||
|
required this.path,
|
||||||
|
required this.itemType,
|
||||||
|
required this.mimeType,
|
||||||
|
required this.itemSource,
|
||||||
required this.shareWith,
|
required this.shareWith,
|
||||||
required this.shareWithDisplayName,
|
required this.shareWithDisplayName,
|
||||||
this.url,
|
this.url,
|
||||||
|
@ -68,8 +100,12 @@ class Share with EquatableMixin {
|
||||||
toString() {
|
toString() {
|
||||||
return "$runtimeType {"
|
return "$runtimeType {"
|
||||||
"id: $id, "
|
"id: $id, "
|
||||||
"path: $path, "
|
|
||||||
"shareType: $shareType, "
|
"shareType: $shareType, "
|
||||||
|
"stime: $stime, "
|
||||||
|
"path: $path, "
|
||||||
|
"itemType: $itemType, "
|
||||||
|
"mimeType: $mimeType, "
|
||||||
|
"itemSource: $itemSource, "
|
||||||
"shareWith: $shareWith, "
|
"shareWith: $shareWith, "
|
||||||
"shareWithDisplayName: $shareWithDisplayName, "
|
"shareWithDisplayName: $shareWithDisplayName, "
|
||||||
"url: $url, "
|
"url: $url, "
|
||||||
|
@ -79,21 +115,34 @@ class Share with EquatableMixin {
|
||||||
@override
|
@override
|
||||||
get props => [
|
get props => [
|
||||||
id,
|
id,
|
||||||
path,
|
|
||||||
shareType,
|
shareType,
|
||||||
|
stime,
|
||||||
|
path,
|
||||||
|
itemType,
|
||||||
|
mimeType,
|
||||||
|
itemSource,
|
||||||
shareWith,
|
shareWith,
|
||||||
shareWithDisplayName,
|
shareWithDisplayName,
|
||||||
url,
|
url,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// see: https://doc.owncloud.com/server/latest/developer_manual/core/apis/ocs-share-api.html#response-attributes-2
|
||||||
final String id;
|
final String id;
|
||||||
final String path;
|
|
||||||
final ShareType shareType;
|
final ShareType shareType;
|
||||||
|
final DateTime stime;
|
||||||
|
final String path;
|
||||||
|
final ShareItemType itemType;
|
||||||
|
final String mimeType;
|
||||||
|
final int itemSource;
|
||||||
final String? shareWith;
|
final String? shareWith;
|
||||||
final String shareWithDisplayName;
|
final String shareWithDisplayName;
|
||||||
final String? url;
|
final String? url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ShareExtension on Share {
|
||||||
|
String get filename => path_util.basename(path);
|
||||||
|
}
|
||||||
|
|
||||||
class ShareRepo {
|
class ShareRepo {
|
||||||
ShareRepo(this.dataSrc);
|
ShareRepo(this.dataSrc);
|
||||||
|
|
||||||
|
@ -105,6 +154,9 @@ class ShareRepo {
|
||||||
Future<List<Share>> listDir(Account account, File dir) =>
|
Future<List<Share>> listDir(Account account, File dir) =>
|
||||||
dataSrc.listDir(account, dir);
|
dataSrc.listDir(account, dir);
|
||||||
|
|
||||||
|
/// See [ShareDataSource.listAll]
|
||||||
|
Future<List<Share>> listAll(Account account) => dataSrc.listAll(account);
|
||||||
|
|
||||||
/// See [ShareDataSource.create]
|
/// See [ShareDataSource.create]
|
||||||
Future<Share> create(Account account, File file, String shareWith) =>
|
Future<Share> create(Account account, File file, String shareWith) =>
|
||||||
dataSrc.create(account, file, shareWith);
|
dataSrc.create(account, file, shareWith);
|
||||||
|
@ -131,6 +183,9 @@ abstract class ShareDataSource {
|
||||||
/// List all shares from a given directory
|
/// List all shares from a given directory
|
||||||
Future<List<Share>> listDir(Account account, File dir);
|
Future<List<Share>> listDir(Account account, File dir);
|
||||||
|
|
||||||
|
/// List all shares from a given user
|
||||||
|
Future<List<Share>> listAll(Account account);
|
||||||
|
|
||||||
/// Share a file/folder with a user
|
/// Share a file/folder with a user
|
||||||
Future<Share> create(Account account, File file, String shareWith);
|
Future<Share> create(Account account, File file, String shareWith);
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,13 @@ class ShareRemoteDataSource implements ShareDataSource {
|
||||||
return _onListResult(response);
|
return _onListResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
listAll(Account account) async {
|
||||||
|
_log.info("[listAll] $account");
|
||||||
|
final response = await Api(account).ocs().filesSharing().shares().get();
|
||||||
|
return _onListResult(response);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
create(Account account, File file, String shareWith) async {
|
create(Account account, File file, String shareWith) async {
|
||||||
_log.info("[create] Share '${file.path}' with '$shareWith'");
|
_log.info("[create] Share '${file.path}' with '$shareWith'");
|
||||||
|
@ -116,10 +123,16 @@ class _ShareParser {
|
||||||
|
|
||||||
Share parseSingle(JsonObj json) {
|
Share parseSingle(JsonObj json) {
|
||||||
final shareType = ShareTypeExtension.fromValue(json["share_type"]);
|
final shareType = ShareTypeExtension.fromValue(json["share_type"]);
|
||||||
|
final itemType = ShareItemTypeExtension.fromValue(json["item_type"]);
|
||||||
return Share(
|
return Share(
|
||||||
id: json["id"],
|
id: json["id"],
|
||||||
path: json["path"],
|
|
||||||
shareType: shareType,
|
shareType: shareType,
|
||||||
|
stime:
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(json["stime"] * 1000),
|
||||||
|
path: json["path"],
|
||||||
|
itemType: itemType,
|
||||||
|
mimeType: json["mimetype"],
|
||||||
|
itemSource: json["item_source"],
|
||||||
// when shared with a password protected link, shareWith somehow contains
|
// when shared with a password protected link, shareWith somehow contains
|
||||||
// the password, which doesn't make sense. We set it to null instead
|
// the password, which doesn't make sense. We set it to null instead
|
||||||
shareWith: shareType == ShareType.publicLink ? null : json["share_with"],
|
shareWith: shareType == ShareType.publicLink ? null : json["share_with"],
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/share.dart';
|
||||||
import 'package:nc_photos/metadata_task_manager.dart';
|
import 'package:nc_photos/metadata_task_manager.dart';
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
|
|
||||||
|
@ -85,6 +86,13 @@ class FileMovedEvent {
|
||||||
final String destination;
|
final String destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShareRemovedEvent {
|
||||||
|
const ShareRemovedEvent(this.account, this.share);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final Share share;
|
||||||
|
}
|
||||||
|
|
||||||
class ThemeChangedEvent {}
|
class ThemeChangedEvent {}
|
||||||
|
|
||||||
class LanguageChangedEvent {}
|
class LanguageChangedEvent {}
|
||||||
|
|
|
@ -856,6 +856,36 @@
|
||||||
"description": "Create a password protected share link on server and share it"
|
"description": "Create a password protected share link on server and share it"
|
||||||
},
|
},
|
||||||
"shareMethodPasswordLinkDescription": "Create a new password protected link on the server",
|
"shareMethodPasswordLinkDescription": "Create a new password protected link on the server",
|
||||||
|
"collectionSharingLabel": "Sharing",
|
||||||
|
"@collectionSharingLabel": {
|
||||||
|
"description": "List items being shared by the current account"
|
||||||
|
},
|
||||||
|
"fileLastSharedDescription": "Last shared on {date}",
|
||||||
|
"@fileLastSharedDescription": {
|
||||||
|
"description": "The date when this file is last shared. The date string is formatted according to the current locale",
|
||||||
|
"placeholders": {
|
||||||
|
"date": {
|
||||||
|
"example": "Jan 1, 2021"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharedWithLabel": "Shared with",
|
||||||
|
"@sharedWithLabel": {
|
||||||
|
"description": "A list of users or links where this file is sharing with"
|
||||||
|
},
|
||||||
|
"unshareTooltip": "Unshare",
|
||||||
|
"@unshareTooltip": {
|
||||||
|
"description": "Remove a share"
|
||||||
|
},
|
||||||
|
"unshareSuccessNotification": "Removed share",
|
||||||
|
"@unshareSuccessNotification": {
|
||||||
|
"description": "Removed a share"
|
||||||
|
},
|
||||||
|
"locationLabel": "Location",
|
||||||
|
"@locationLabel": {
|
||||||
|
"description": "Show where the file is located"
|
||||||
|
},
|
||||||
|
|
||||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||||
"@errorUnauthenticated": {
|
"@errorUnauthenticated": {
|
||||||
"description": "Error message when server responds with HTTP401"
|
"description": "Error message when server responds with HTTP401"
|
||||||
|
|
|
@ -5,7 +5,13 @@
|
||||||
"settingsAlbumPageTitle",
|
"settingsAlbumPageTitle",
|
||||||
"settingsShowDateInAlbumTitle",
|
"settingsShowDateInAlbumTitle",
|
||||||
"settingsShowDateInAlbumDescription",
|
"settingsShowDateInAlbumDescription",
|
||||||
"sortOptionManualLabel"
|
"sortOptionManualLabel",
|
||||||
|
"collectionSharingLabel",
|
||||||
|
"fileLastSharedDescription",
|
||||||
|
"sharedWithLabel",
|
||||||
|
"unshareTooltip",
|
||||||
|
"unshareSuccessNotification",
|
||||||
|
"locationLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
|
@ -28,7 +34,13 @@
|
||||||
"shareMethodPublicLinkTitle",
|
"shareMethodPublicLinkTitle",
|
||||||
"shareMethodPublicLinkDescription",
|
"shareMethodPublicLinkDescription",
|
||||||
"shareMethodPasswordLinkTitle",
|
"shareMethodPasswordLinkTitle",
|
||||||
"shareMethodPasswordLinkDescription"
|
"shareMethodPasswordLinkDescription",
|
||||||
|
"collectionSharingLabel",
|
||||||
|
"fileLastSharedDescription",
|
||||||
|
"sharedWithLabel",
|
||||||
|
"unshareTooltip",
|
||||||
|
"unshareSuccessNotification",
|
||||||
|
"locationLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"el": [
|
"el": [
|
||||||
|
@ -106,7 +118,13 @@
|
||||||
"shareMethodPublicLinkTitle",
|
"shareMethodPublicLinkTitle",
|
||||||
"shareMethodPublicLinkDescription",
|
"shareMethodPublicLinkDescription",
|
||||||
"shareMethodPasswordLinkTitle",
|
"shareMethodPasswordLinkTitle",
|
||||||
"shareMethodPasswordLinkDescription"
|
"shareMethodPasswordLinkDescription",
|
||||||
|
"collectionSharingLabel",
|
||||||
|
"fileLastSharedDescription",
|
||||||
|
"sharedWithLabel",
|
||||||
|
"unshareTooltip",
|
||||||
|
"unshareSuccessNotification",
|
||||||
|
"locationLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
|
@ -115,7 +133,13 @@
|
||||||
"settingsAlbumPageTitle",
|
"settingsAlbumPageTitle",
|
||||||
"settingsShowDateInAlbumTitle",
|
"settingsShowDateInAlbumTitle",
|
||||||
"settingsShowDateInAlbumDescription",
|
"settingsShowDateInAlbumDescription",
|
||||||
"sortOptionManualLabel"
|
"sortOptionManualLabel",
|
||||||
|
"collectionSharingLabel",
|
||||||
|
"fileLastSharedDescription",
|
||||||
|
"sharedWithLabel",
|
||||||
|
"unshareTooltip",
|
||||||
|
"unshareSuccessNotification",
|
||||||
|
"locationLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
|
@ -173,7 +197,13 @@
|
||||||
"shareMethodPublicLinkTitle",
|
"shareMethodPublicLinkTitle",
|
||||||
"shareMethodPublicLinkDescription",
|
"shareMethodPublicLinkDescription",
|
||||||
"shareMethodPasswordLinkTitle",
|
"shareMethodPasswordLinkTitle",
|
||||||
"shareMethodPasswordLinkDescription"
|
"shareMethodPasswordLinkDescription",
|
||||||
|
"collectionSharingLabel",
|
||||||
|
"fileLastSharedDescription",
|
||||||
|
"sharedWithLabel",
|
||||||
|
"unshareTooltip",
|
||||||
|
"unshareSuccessNotification",
|
||||||
|
"locationLabel"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -204,6 +234,12 @@
|
||||||
"shareMethodPublicLinkTitle",
|
"shareMethodPublicLinkTitle",
|
||||||
"shareMethodPublicLinkDescription",
|
"shareMethodPublicLinkDescription",
|
||||||
"shareMethodPasswordLinkTitle",
|
"shareMethodPasswordLinkTitle",
|
||||||
"shareMethodPasswordLinkDescription"
|
"shareMethodPasswordLinkDescription",
|
||||||
|
"collectionSharingLabel",
|
||||||
|
"fileLastSharedDescription",
|
||||||
|
"sharedWithLabel",
|
||||||
|
"unshareTooltip",
|
||||||
|
"unshareSuccessNotification",
|
||||||
|
"locationLabel"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
27
lib/use_case/find_file.dart
Normal file
27
lib/use_case/find_file.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:idb_shim/idb_client.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/app_db.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
|
||||||
|
class FindFile {
|
||||||
|
/// Find the [File] in the DB by [fileId]
|
||||||
|
Future<File> call(Account account, int fileId) async {
|
||||||
|
return await AppDb.use((db) async {
|
||||||
|
final transaction =
|
||||||
|
db.transaction(AppDb.fileDbStoreName, idbModeReadOnly);
|
||||||
|
final store = transaction.objectStore(AppDb.fileDbStoreName);
|
||||||
|
final index = store.index(AppDbFileDbEntry.indexName);
|
||||||
|
final List dbItems = await index
|
||||||
|
.getAll(AppDbFileDbEntry.toNamespacedFileId(account, fileId));
|
||||||
|
|
||||||
|
// find the one owned by us
|
||||||
|
final dbItem = dbItems.firstWhere((element) {
|
||||||
|
final e = AppDbFileDbEntry.fromJson(element.cast<String, dynamic>());
|
||||||
|
return file_util.getUserDirName(e.file) == account.username;
|
||||||
|
});
|
||||||
|
final dbEntry = AppDbFileDbEntry.fromJson(dbItem.cast<String, dynamic>());
|
||||||
|
return dbEntry.file;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
16
lib/use_case/remove_share.dart
Normal file
16
lib/use_case/remove_share.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:event_bus/event_bus.dart';
|
||||||
|
import 'package:kiwi/kiwi.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/entity/share.dart';
|
||||||
|
import 'package:nc_photos/event/event.dart';
|
||||||
|
|
||||||
|
class RemoveShare {
|
||||||
|
const RemoveShare(this.shareRepo);
|
||||||
|
|
||||||
|
Future<void> call(Account account, Share share) async {
|
||||||
|
await shareRepo.delete(account, share);
|
||||||
|
KiwiContainer().resolve<EventBus>().fire(ShareRemovedEvent(account, share));
|
||||||
|
}
|
||||||
|
|
||||||
|
final ShareRepo shareRepo;
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import 'package:nc_photos/widget/pending_albums.dart';
|
||||||
import 'package:nc_photos/widget/people_browser.dart';
|
import 'package:nc_photos/widget/people_browser.dart';
|
||||||
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
||||||
import 'package:nc_photos/widget/selection_app_bar.dart';
|
import 'package:nc_photos/widget/selection_app_bar.dart';
|
||||||
|
import 'package:nc_photos/widget/sharing_browser.dart';
|
||||||
import 'package:nc_photos/widget/trashbin_browser.dart';
|
import 'package:nc_photos/widget/trashbin_browser.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -237,6 +238,19 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SelectableItem _buildSharingItem(BuildContext context) {
|
||||||
|
return _ButtonListItem(
|
||||||
|
icon: Icons.share_outlined,
|
||||||
|
label: L10n.global().collectionSharingLabel,
|
||||||
|
onTap: () {
|
||||||
|
if (!isSelectionMode) {
|
||||||
|
Navigator.of(context).pushNamed(SharingBrowser.routeName,
|
||||||
|
arguments: SharingBrowserArguments(widget.account));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
SelectableItem _buildShareItem(BuildContext context) {
|
SelectableItem _buildShareItem(BuildContext context) {
|
||||||
return _ButtonListItem(
|
return _ButtonListItem(
|
||||||
icon: Icons.share_outlined,
|
icon: Icons.share_outlined,
|
||||||
|
@ -444,6 +458,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
}).map((e) => e.item2);
|
}).map((e) => e.item2);
|
||||||
itemStreamListItems = [
|
itemStreamListItems = [
|
||||||
_buildPersonItem(context),
|
_buildPersonItem(context),
|
||||||
|
_buildSharingItem(context),
|
||||||
if (Lab().enableSharedAlbum) _buildShareItem(context),
|
if (Lab().enableSharedAlbum) _buildShareItem(context),
|
||||||
_buildArchiveItem(context),
|
_buildArchiveItem(context),
|
||||||
_buildTrashbinItem(context),
|
_buildTrashbinItem(context),
|
||||||
|
|
|
@ -22,6 +22,8 @@ import 'package:nc_photos/widget/person_browser.dart';
|
||||||
import 'package:nc_photos/widget/root_picker.dart';
|
import 'package:nc_photos/widget/root_picker.dart';
|
||||||
import 'package:nc_photos/widget/settings.dart';
|
import 'package:nc_photos/widget/settings.dart';
|
||||||
import 'package:nc_photos/widget/setup.dart';
|
import 'package:nc_photos/widget/setup.dart';
|
||||||
|
import 'package:nc_photos/widget/shared_file_viewer.dart';
|
||||||
|
import 'package:nc_photos/widget/sharing_browser.dart';
|
||||||
import 'package:nc_photos/widget/sign_in.dart';
|
import 'package:nc_photos/widget/sign_in.dart';
|
||||||
import 'package:nc_photos/widget/slideshow_viewer.dart';
|
import 'package:nc_photos/widget/slideshow_viewer.dart';
|
||||||
import 'package:nc_photos/widget/splash.dart';
|
import 'package:nc_photos/widget/splash.dart';
|
||||||
|
@ -145,6 +147,8 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
route ??= _handlePeopleBrowserRoute(settings);
|
route ??= _handlePeopleBrowserRoute(settings);
|
||||||
route ??= _handlePersonBrowserRoute(settings);
|
route ??= _handlePersonBrowserRoute(settings);
|
||||||
route ??= _handleSlideshowViewerRoute(settings);
|
route ??= _handleSlideshowViewerRoute(settings);
|
||||||
|
route ??= _handleSharingBrowserRoute(settings);
|
||||||
|
route ??= _handleSharedFileViewerRoute(settings);
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,6 +380,34 @@ class _MyAppState extends State<MyApp> implements SnackBarHandler {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Route<dynamic>? _handleSharingBrowserRoute(RouteSettings settings) {
|
||||||
|
try {
|
||||||
|
if (settings.name == SharingBrowser.routeName &&
|
||||||
|
settings.arguments != null) {
|
||||||
|
final args = settings.arguments as SharingBrowserArguments;
|
||||||
|
return SharingBrowser.buildRoute(args);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe(
|
||||||
|
"[_handleSharingBrowserRoute] Failed while handling route", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Route<dynamic>? _handleSharedFileViewerRoute(RouteSettings settings) {
|
||||||
|
try {
|
||||||
|
if (settings.name == SharedFileViewer.routeName &&
|
||||||
|
settings.arguments != null) {
|
||||||
|
final args = settings.arguments as SharedFileViewerArguments;
|
||||||
|
return SharedFileViewer.buildRoute(args);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe(
|
||||||
|
"[_handleSharedFileViewerRoute] Failed while handling route", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
|
|
||||||
late AppEventListener<ThemeChangedEvent> _themeChangedListener;
|
late AppEventListener<ThemeChangedEvent> _themeChangedListener;
|
||||||
|
|
266
lib/widget/shared_file_viewer.dart
Normal file
266
lib/widget/shared_file_viewer.dart
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/api/api.dart';
|
||||||
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/cache_manager_util.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:nc_photos/entity/share.dart';
|
||||||
|
import 'package:nc_photos/entity/share/data_source.dart';
|
||||||
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/use_case/remove_share.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
class SharedFileViewerArguments {
|
||||||
|
SharedFileViewerArguments(this.account, this.file, this.shares);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final File file;
|
||||||
|
final List<Share> shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle shares associated with a [File]
|
||||||
|
class SharedFileViewer extends StatefulWidget {
|
||||||
|
static const routeName = "/shared-file-viewer";
|
||||||
|
|
||||||
|
static Route buildRoute(SharedFileViewerArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => SharedFileViewer.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SharedFileViewer({
|
||||||
|
Key? key,
|
||||||
|
required this.account,
|
||||||
|
required this.file,
|
||||||
|
required this.shares,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
SharedFileViewer.fromArgs(SharedFileViewerArguments args, {Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
account: args.account,
|
||||||
|
file: args.file,
|
||||||
|
shares: args.shares,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _SharedFileViewerState();
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final File file;
|
||||||
|
final List<Share> shares;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SharedFileViewerState extends State<SharedFileViewer> {
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return AppTheme(
|
||||||
|
child: Scaffold(
|
||||||
|
body: _buildContent(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context) {
|
||||||
|
final previewUrl = api_util.getFilePreviewUrl(
|
||||||
|
widget.account,
|
||||||
|
widget.file,
|
||||||
|
width: k.photoLargeSize,
|
||||||
|
height: k.photoLargeSize,
|
||||||
|
a: true,
|
||||||
|
);
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
title: Text(path.withoutExtension(widget.file.filename)),
|
||||||
|
pinned: true,
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 256,
|
||||||
|
child: FittedBox(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
cacheManager: CoverCacheManager.inst,
|
||||||
|
imageUrl: previewUrl,
|
||||||
|
httpHeaders: {
|
||||||
|
"Authorization":
|
||||||
|
Api.getAuthorizationHeaderValue(widget.account),
|
||||||
|
},
|
||||||
|
fadeInDuration: const Duration(),
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
errorWidget: (context, url, error) {
|
||||||
|
// just leave it empty
|
||||||
|
return Container();
|
||||||
|
},
|
||||||
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
L10n.global().locationLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(widget.file.strippedPath),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
L10n.global().sharedWithLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => _buildShareItem(context, _shares[index]),
|
||||||
|
childCount: _shares.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShareItem(BuildContext context, Share share) {
|
||||||
|
final dateStr = DateFormat(DateFormat.YEAR_ABBR_MONTH_DAY,
|
||||||
|
Localizations.localeOf(context).languageCode)
|
||||||
|
.format(share.stime.toLocal());
|
||||||
|
return ListTile(
|
||||||
|
title: Text(_getShareTitle(share)),
|
||||||
|
subtitle: Text(dateStr),
|
||||||
|
leading: SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
child: Icon(_getShareIcon(share)),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (share.shareType == ShareType.publicLink)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _onItemCopyPressed(share),
|
||||||
|
icon: Tooltip(
|
||||||
|
message: MaterialLocalizations.of(context).copyButtonLabel,
|
||||||
|
child: const Icon(Icons.copy_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton<_ItemMenuOption>(
|
||||||
|
tooltip: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||||
|
itemBuilder: (_) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: _ItemMenuOption.unshare,
|
||||||
|
child: Text(L10n.global().unshareTooltip),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (option) =>
|
||||||
|
_onItemMenuOptionSelected(context, share, option),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onItemCopyPressed(Share share) async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: share.url!));
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(L10n.global().linkCopiedNotification),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemMenuOptionSelected(
|
||||||
|
BuildContext context, Share share, _ItemMenuOption option) {
|
||||||
|
switch (option) {
|
||||||
|
case _ItemMenuOption.unshare:
|
||||||
|
_onItemUnsharePressed(context, share);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onItemUnsharePressed(BuildContext context, Share share) async {
|
||||||
|
final shareRepo = ShareRepo(ShareRemoteDataSource());
|
||||||
|
try {
|
||||||
|
await RemoveShare(shareRepo)(widget.account, share);
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(L10n.global().unshareSuccessNotification),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
if (_shares.length == 1) {
|
||||||
|
// removing last share
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_shares.remove(share);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout(
|
||||||
|
"[_onItemUnsharePressed] Failed while RemoveShare", e, stackTrace);
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(e)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData? _getShareIcon(Share share) {
|
||||||
|
switch (share.shareType) {
|
||||||
|
case ShareType.user:
|
||||||
|
case ShareType.email:
|
||||||
|
case ShareType.federatedCloudShare:
|
||||||
|
return Icons.person_outlined;
|
||||||
|
case ShareType.group:
|
||||||
|
case ShareType.circle:
|
||||||
|
return Icons.group_outlined;
|
||||||
|
case ShareType.publicLink:
|
||||||
|
return Icons.link_outlined;
|
||||||
|
case ShareType.talk:
|
||||||
|
return Icons.sms_outlined;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getShareTitle(Share share) {
|
||||||
|
if (share.shareType == ShareType.publicLink) {
|
||||||
|
if (share.url!.startsWith(widget.account.url)) {
|
||||||
|
return share.url!.substring(widget.account.url.length);
|
||||||
|
} else {
|
||||||
|
return share.url!;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return share.shareWithDisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
late final List<Share> _shares = List.of(widget.shares);
|
||||||
|
|
||||||
|
static final _log =
|
||||||
|
Logger("widget.shared_file_viewer._SharedFileViewerState");
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _ItemMenuOption {
|
||||||
|
unshare,
|
||||||
|
}
|
251
lib/widget/sharing_browser.dart
Normal file
251
lib/widget/sharing_browser.dart
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/api/api.dart';
|
||||||
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/bloc/list_sharing.dart';
|
||||||
|
import 'package:nc_photos/cache_manager_util.dart';
|
||||||
|
import 'package:nc_photos/entity/share.dart';
|
||||||
|
import 'package:nc_photos/event/event.dart';
|
||||||
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/widget/empty_list_indicator.dart';
|
||||||
|
import 'package:nc_photos/widget/shared_file_viewer.dart';
|
||||||
|
|
||||||
|
class SharingBrowserArguments {
|
||||||
|
SharingBrowserArguments(this.account);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a list of all shares associated with this account
|
||||||
|
class SharingBrowser extends StatefulWidget {
|
||||||
|
static const routeName = "/sharing-browser";
|
||||||
|
|
||||||
|
static Route buildRoute(SharingBrowserArguments args) => MaterialPageRoute(
|
||||||
|
builder: (context) => SharingBrowser.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
|
const SharingBrowser({
|
||||||
|
Key? key,
|
||||||
|
required this.account,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
SharingBrowser.fromArgs(SharingBrowserArguments args, {Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
account: args.account,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _SharingBrowserState();
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SharingBrowserState extends State<SharingBrowser> {
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_initBloc();
|
||||||
|
_shareRemovedListener.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
_shareRemovedListener.end();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return AppTheme(
|
||||||
|
child: Scaffold(
|
||||||
|
body: BlocListener<ListSharingBloc, ListSharingBlocState>(
|
||||||
|
bloc: _bloc,
|
||||||
|
listener: (context, state) => _onStateChange(context, state),
|
||||||
|
child: BlocBuilder<ListSharingBloc, ListSharingBlocState>(
|
||||||
|
bloc: _bloc,
|
||||||
|
builder: (context, state) => _buildContent(context, state),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initBloc() {
|
||||||
|
if (_bloc.state is ListSharingBlocInit) {
|
||||||
|
_log.info("[_initBloc] Initialize bloc");
|
||||||
|
} else {
|
||||||
|
// process the current state
|
||||||
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||||
|
setState(() {
|
||||||
|
_onStateChange(context, _bloc.state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_reqQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context, ListSharingBlocState state) {
|
||||||
|
if ((state is ListSharingBlocSuccess || state is ListSharingBlocFailure) &&
|
||||||
|
state.items.isEmpty) {
|
||||||
|
return _buildEmptyContent(context);
|
||||||
|
} else {
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
title: Text(L10n.global().collectionSharingLabel),
|
||||||
|
floating: true,
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => _buildItem(context, _items[index]),
|
||||||
|
childCount: _items.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyContent(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
title: Text(L10n.global().collectionSharingLabel),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: EmptyListIndicator(
|
||||||
|
icon: Icons.share_outlined,
|
||||||
|
text: L10n.global().listEmptyText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(BuildContext context, List<ListSharingItem> shares) {
|
||||||
|
const leadingSize = 56.0;
|
||||||
|
final dateStr = DateFormat(DateFormat.YEAR_ABBR_MONTH_DAY,
|
||||||
|
Localizations.localeOf(context).languageCode)
|
||||||
|
.format(shares.first.share.stime.toLocal());
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(SharedFileViewer.routeName,
|
||||||
|
arguments: SharedFileViewerArguments(
|
||||||
|
widget.account,
|
||||||
|
shares.first.file,
|
||||||
|
shares.map((e) => e.share).toList(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
shares.first.share.itemType == ShareItemType.folder
|
||||||
|
? const Icon(
|
||||||
|
Icons.folder_outlined,
|
||||||
|
size: leadingSize,
|
||||||
|
)
|
||||||
|
: CachedNetworkImage(
|
||||||
|
width: leadingSize,
|
||||||
|
height: leadingSize,
|
||||||
|
cacheManager: ThumbnailCacheManager.inst,
|
||||||
|
imageUrl: api_util.getFilePreviewUrl(
|
||||||
|
widget.account, shares.first.file,
|
||||||
|
width: k.photoThumbSize, height: k.photoThumbSize),
|
||||||
|
httpHeaders: {
|
||||||
|
"Authorization":
|
||||||
|
Api.getAuthorizationHeaderValue(widget.account),
|
||||||
|
},
|
||||||
|
fadeInDuration: const Duration(),
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
shares.first.share.filename,
|
||||||
|
style: Theme.of(context).textTheme.subtitle1,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
L10n.global().fileLastSharedDescription(dateStr),
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (shares.any((element) => element.share.url?.isNotEmpty == true))
|
||||||
|
const Icon(Icons.link)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStateChange(BuildContext context, ListSharingBlocState state) {
|
||||||
|
if (state is ListSharingBlocInit) {
|
||||||
|
_items = [];
|
||||||
|
} else if (state is ListSharingBlocSuccess ||
|
||||||
|
state is ListSharingBlocLoading) {
|
||||||
|
_transformItems(state.items);
|
||||||
|
} else if (state is ListSharingBlocFailure) {
|
||||||
|
_transformItems(state.items);
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(state.exception)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onShareRemovedEvent(ShareRemovedEvent ev) {}
|
||||||
|
|
||||||
|
void _transformItems(List<ListSharingItem> items) {
|
||||||
|
// group shares of the same file
|
||||||
|
final map = <String, List<ListSharingItem>>{};
|
||||||
|
for (final i in items) {
|
||||||
|
map[i.share.path] ??= <ListSharingItem>[];
|
||||||
|
map[i.share.path]!.add(i);
|
||||||
|
}
|
||||||
|
// sort the sub-lists
|
||||||
|
for (final list in map.values) {
|
||||||
|
list.sort((a, b) => b.share.stime.compareTo(a.share.stime));
|
||||||
|
}
|
||||||
|
// then sort the map and convert it to list
|
||||||
|
_items = map.entries
|
||||||
|
.sorted((a, b) =>
|
||||||
|
b.value.first.share.stime.compareTo(a.value.first.share.stime))
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reqQuery() {
|
||||||
|
_bloc.add(ListSharingBlocQuery(widget.account));
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _bloc = ListSharingBloc.of(widget.account);
|
||||||
|
late final _shareRemovedListener =
|
||||||
|
AppEventListener<ShareRemovedEvent>(_onShareRemovedEvent);
|
||||||
|
|
||||||
|
var _items = <List<ListSharingItem>>[];
|
||||||
|
|
||||||
|
static final _log = Logger("widget.sharing_browser._SharingBrowserState");
|
||||||
|
}
|
Loading…
Reference in a new issue