kopia lustrzana https://gitlab.com/mysocialportal/relatica
Merge branch 'gallery-crud-updates' into 'main'
Gallery and image CRUD updates See merge request mysocialportal/relatica!37codemagic-setup
commit
6be994077b
|
@ -18,7 +18,7 @@ final _useMediaKit = Platform.isAndroid ||
|
|||
Platform.isIOS ||
|
||||
Platform.isWindows ||
|
||||
Platform.isMacOS ||
|
||||
Platform.isLinux;
|
||||
(kReleaseMode && Platform.isLinux);
|
||||
|
||||
class AVControl extends StatefulWidget {
|
||||
final String videoUrl;
|
||||
|
|
|
@ -150,6 +150,25 @@ class GalleryClient extends FriendicaClient {
|
|||
_networkStatusService.finishGalleryLoading();
|
||||
return result;
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> renameGallery(
|
||||
String oldGalleryName, String newGalleryName) async {
|
||||
_networkStatusService.startGalleryLoading();
|
||||
_logger.finest(() => 'Getting gallery data');
|
||||
final url =
|
||||
Uri.parse('https://$serverName/api/friendica/photoalbum/update');
|
||||
final body = {
|
||||
'album': oldGalleryName,
|
||||
'album_new': newGalleryName,
|
||||
};
|
||||
final result = await postUrl(
|
||||
url,
|
||||
body,
|
||||
headers: _headers,
|
||||
).transform((_) => true);
|
||||
_networkStatusService.finishGalleryLoading();
|
||||
return result.execErrorCast();
|
||||
}
|
||||
}
|
||||
|
||||
class GroupsClient extends FriendicaClient {
|
||||
|
@ -272,6 +291,35 @@ class GroupsClient extends FriendicaClient {
|
|||
}
|
||||
}
|
||||
|
||||
class ImageClient extends FriendicaClient {
|
||||
ImageClient(super.credentials) : super();
|
||||
|
||||
FutureResult<ImageEntry, ExecError> editImageData(ImageEntry image) async {
|
||||
_networkStatusService.startGalleryLoading();
|
||||
final uri = Uri.parse('https://$serverName/api/friendica/photo/update');
|
||||
final body = {
|
||||
'album': image.album,
|
||||
'desc': image.description,
|
||||
'photo_id': image.id,
|
||||
};
|
||||
|
||||
final result = await postUrl(uri, body, headers: _headers)
|
||||
.andThen((_) => Result.ok(image));
|
||||
_networkStatusService.finishGalleryLoading();
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<ImageEntry, ExecError> deleteImage(ImageEntry image) async {
|
||||
final uri = Uri.parse(
|
||||
'https://$serverName/api/friendica/photo/delete?photo_id=${image.id}',
|
||||
);
|
||||
|
||||
final result = await postUrl(uri, {}, headers: _headers)
|
||||
.andThen((_) => Result.ok(image));
|
||||
return result.execErrorCast();
|
||||
}
|
||||
}
|
||||
|
||||
class InteractionsClient extends FriendicaClient {
|
||||
static final _logger = Logger('$InteractionsClient');
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ Future<bool?> showConfirmDialog(BuildContext context, String caption) {
|
|||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(caption),
|
||||
content: Text(caption),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
child: const Text('OK'),
|
||||
|
@ -46,7 +46,7 @@ Future<bool?> showYesNoDialog(BuildContext context, String caption) {
|
|||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(caption),
|
||||
content: Text(caption),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
child: const Text('Yes'),
|
||||
|
@ -76,7 +76,7 @@ Future<String?> showChooseOptions(
|
|||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(caption),
|
||||
content: Text(caption),
|
||||
actions: options
|
||||
.map((o) => ElevatedButton(
|
||||
child: Text(o),
|
||||
|
|
|
@ -4,4 +4,10 @@ class GalleryData {
|
|||
final DateTime created;
|
||||
|
||||
GalleryData({required this.count, required this.name, required this.created});
|
||||
|
||||
GalleryData copy({String? name}) => GalleryData(
|
||||
count: count,
|
||||
name: name ?? this.name,
|
||||
created: created,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,22 @@ class ImageEntry {
|
|||
required this.scales,
|
||||
});
|
||||
|
||||
ImageEntry copy({
|
||||
String? description,
|
||||
}) =>
|
||||
ImageEntry(
|
||||
id: id,
|
||||
album: album,
|
||||
filename: filename,
|
||||
description: description ?? this.description,
|
||||
thumbnailUrl: thumbnailUrl,
|
||||
created: created,
|
||||
height: height,
|
||||
width: width,
|
||||
visibility: visibility,
|
||||
scales: scales,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'screens/group_create_screen.dart';
|
|||
import 'screens/group_editor_screen.dart';
|
||||
import 'screens/group_management_screen.dart';
|
||||
import 'screens/home.dart';
|
||||
import 'screens/image_editor_screen.dart';
|
||||
import 'screens/interactions_viewer_screen.dart';
|
||||
import 'screens/message_thread_screen.dart';
|
||||
import 'screens/message_threads_browser_screen.dart';
|
||||
|
@ -162,9 +163,16 @@ final appRouter = GoRouter(
|
|||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'show/:name',
|
||||
path: 'show',
|
||||
builder: (context, state) => GalleryScreen(
|
||||
galleryName: state.extra!.toString(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'edit/:name/image/:id',
|
||||
builder: (context, state) => ImageEditorScreen(
|
||||
galleryName: state.params['name']!,
|
||||
imageId: state.params['id']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -7,6 +7,7 @@ import '../controls/padding.dart';
|
|||
import '../controls/standard_appbar.dart';
|
||||
import '../controls/status_and_refresh_button.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/gallery_data.dart';
|
||||
import '../services/gallery_service.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
|
@ -14,6 +15,71 @@ import '../utils/active_profile_selector.dart';
|
|||
class GalleryBrowsersScreen extends StatelessWidget {
|
||||
static final _logger = Logger('$GalleryBrowsersScreen');
|
||||
|
||||
String? validNameChecker(String? text) {
|
||||
final newName = text ?? '';
|
||||
if (newName.isEmpty) {
|
||||
return 'Name cannot be empty';
|
||||
}
|
||||
|
||||
if (!RegExp(
|
||||
r"^[a-zA-Z0-9 ]+$",
|
||||
).hasMatch(newName)) {
|
||||
return 'Name must be only letters and numbers';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> renameGallery(
|
||||
BuildContext context,
|
||||
GalleryService service,
|
||||
GalleryData gallery,
|
||||
) async {
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
var controller = TextEditingController(text: gallery.name);
|
||||
return Form(
|
||||
child: AlertDialog(
|
||||
title: const Text('Rename Gallery'),
|
||||
content: TextFormField(
|
||||
controller: controller,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
validator: validNameChecker,
|
||||
),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: () {
|
||||
if (validNameChecker(controller.text) != null) {
|
||||
return;
|
||||
}
|
||||
Navigator.pop(context,
|
||||
controller.text); // showDialog() returns true
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.pop(
|
||||
context, gallery.name); // showDialog() returns true
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
'';
|
||||
|
||||
if (newName.isEmpty || newName == gallery.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
await service.renameGallery(gallery, newName);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building');
|
||||
|
@ -67,17 +133,20 @@ class GalleryBrowsersScreen extends StatelessWidget {
|
|||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
final gallery = galleries[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.push('/gallery/show/${gallery.name}');
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(gallery.name),
|
||||
subtitle: Text(
|
||||
'Created: ${gallery.created}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
return ListTile(
|
||||
onTap: () => context.push('/gallery/show', extra: gallery.name),
|
||||
title: Text(gallery.name),
|
||||
subtitle: Text(
|
||||
'#Photos: ${gallery.count}, Created: ${gallery.created}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
trailing: ElevatedButton(
|
||||
onPressed: () async => await renameGallery(
|
||||
context,
|
||||
service,
|
||||
gallery,
|
||||
),
|
||||
trailing: Text('${gallery.count} Images'),
|
||||
child: const Text('Rename'),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../controls/login_aware_cached_network_image.dart';
|
||||
import '../controls/standard_appbar.dart';
|
||||
|
@ -11,10 +13,10 @@ import '../serializers/friendica/image_entry_friendica_extensions.dart';
|
|||
import '../services/gallery_service.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
import 'media_viewer_screen.dart';
|
||||
|
||||
class GalleryScreen extends StatelessWidget {
|
||||
static const thumbnailDimension = 350.0;
|
||||
static final _logger = Logger('$GalleryScreen');
|
||||
final String galleryName;
|
||||
|
||||
|
@ -22,32 +24,52 @@ class GalleryScreen extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building');
|
||||
_logger.finest('Building $galleryName');
|
||||
final nss = getIt<NetworkStatusService>();
|
||||
final service = context
|
||||
.watch<ActiveProfileSelector<GalleryService>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
final body = service.getGallery(galleryName).fold(
|
||||
onSuccess: (galleryData) =>
|
||||
buildBody(context, service, galleryData.count),
|
||||
onError: (error) => buildErrorBody(error.message),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: StandardAppBar.build(context, galleryName, actions: [
|
||||
StatusAndRefreshButton(
|
||||
valueListenable: nss.imageGalleryLoadingStatus,
|
||||
refreshFunction: () async => await service.updateGalleryImageList(
|
||||
galleryName: galleryName,
|
||||
withNextPage: false,
|
||||
nextPageOnly: false,
|
||||
),
|
||||
refreshFunction: () async => context
|
||||
.read<ActiveProfileSelector<GalleryService>>()
|
||||
.activeEntry
|
||||
.withResultAsync(
|
||||
(gs) async => gs.updateGalleryImageList(
|
||||
galleryName: galleryName,
|
||||
withNextPage: false,
|
||||
nextPageOnly: false,
|
||||
),
|
||||
),
|
||||
busyColor: Theme.of(context).appBarTheme.foregroundColor,
|
||||
),
|
||||
]),
|
||||
body: body,
|
||||
body: _GalleryScreenBody(
|
||||
galleryName: galleryName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GalleryScreenBody extends StatelessWidget {
|
||||
static const thumbnailDimension = 350.0;
|
||||
static final _logger = Logger('$_GalleryScreenBody');
|
||||
final String galleryName;
|
||||
|
||||
const _GalleryScreenBody({required this.galleryName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building');
|
||||
final service = context
|
||||
.watch<ActiveProfileSelector<GalleryService>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
return service.getGallery(galleryName).fold(
|
||||
onSuccess: (galleryData) =>
|
||||
buildBody(context, service, galleryData.count),
|
||||
onError: (error) => buildErrorBody(error.message),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildErrorBody(String error) {
|
||||
return Center(
|
||||
|
@ -131,12 +153,76 @@ class GalleryScreen extends StatelessWidget {
|
|||
height: thumbnailDimension,
|
||||
imageUrl: image.thumbnailUrl,
|
||||
),
|
||||
Positioned(
|
||||
top: 5.0,
|
||||
right: 5.0,
|
||||
child: Row(
|
||||
children: [
|
||||
Card(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.7),
|
||||
child: IconButton(
|
||||
onPressed: () => context.push(
|
||||
'/gallery/edit/$galleryName/image/${image.id}',
|
||||
),
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.7),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
final confirm = await showYesNoDialog(
|
||||
context, 'Delete image?');
|
||||
if (confirm != true) {
|
||||
return;
|
||||
}
|
||||
await service
|
||||
.deleteImage(image)
|
||||
.withError((error) {
|
||||
if (context.mounted) {
|
||||
buildSnackbar(context,
|
||||
'Error deleting image: $error');
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (image.description.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 5.0,
|
||||
left: 5.0,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async => await showImageCaption(
|
||||
context,
|
||||
image.description,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.7)),
|
||||
child: const Text('ALT'),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 5.0,
|
||||
right: 5.0,
|
||||
child: Icon(image.visibility.type == VisibilityType.public
|
||||
? Icons.public
|
||||
: Icons.lock),
|
||||
child: Card(
|
||||
color: Theme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
.withOpacity(0.7),
|
||||
child: Icon(
|
||||
image.visibility.type == VisibilityType.public
|
||||
? Icons.public
|
||||
: Icons.lock),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -145,4 +231,27 @@ class GalleryScreen extends StatelessWidget {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> showImageCaption(BuildContext context, String text) async {
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: Text(
|
||||
text,
|
||||
softWrap: true,
|
||||
),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
child: const Text('Dismiss'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true); // showDialog() returns true
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../controls/login_aware_cached_network_image.dart';
|
||||
import '../controls/padding.dart';
|
||||
import '../controls/responsive_max_width.dart';
|
||||
import '../controls/standard_appbar.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import '../models/visibility.dart';
|
||||
import '../services/gallery_service.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
class ImageEditorScreen extends StatefulWidget {
|
||||
final String galleryName;
|
||||
final String imageId;
|
||||
|
||||
const ImageEditorScreen({
|
||||
super.key,
|
||||
required this.galleryName,
|
||||
required this.imageId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImageEditorScreen> createState() => _ImageEditorScreenState();
|
||||
}
|
||||
|
||||
class _ImageEditorScreenState extends State<ImageEditorScreen> {
|
||||
late final Result<ImageEntry, ExecError> originalImageResult;
|
||||
final altTextController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
originalImageResult = getIt<ActiveProfileSelector<GalleryService>>()
|
||||
.activeEntry
|
||||
.andThen((gs) => gs.getImage(widget.galleryName, widget.imageId))
|
||||
.withResult((image) {
|
||||
altTextController.text = image.description;
|
||||
}).execErrorCast();
|
||||
}
|
||||
|
||||
bool get changed => originalImageResult
|
||||
.transform((image) => image.description != altTextController.text)
|
||||
.getValueOrElse(() => false);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: StandardAppBar.build(
|
||||
context,
|
||||
'Edit Image',
|
||||
withDrawer: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: ResponsiveMaxWidth(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
...originalImageResult.fold(
|
||||
onSuccess: (image) => buildEditor(image),
|
||||
onError: (error) => buildError(error),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
final result = await getIt<
|
||||
ActiveProfileSelector<GalleryService>>()
|
||||
.activeEntry
|
||||
.andThenAsync(
|
||||
(gs) async => await gs
|
||||
.updateImage(originalImageResult.value.copy(
|
||||
description: altTextController.text,
|
||||
)),
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.match(
|
||||
onSuccess: (_) => context.pop(),
|
||||
onError: (error) => buildSnackbar(context,
|
||||
'Error attempting to update image: $error'),
|
||||
);
|
||||
},
|
||||
child: const Text('Save')),
|
||||
const HorizontalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (!changed) {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
final ok = await showYesNoDialog(
|
||||
context,
|
||||
'Cancel changes?',
|
||||
);
|
||||
if (ok == true && mounted) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
child: const Text('Cancel')),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildEditor(ImageEntry originalImage) {
|
||||
return [
|
||||
Row(
|
||||
children: [
|
||||
const Text('Visibility:'),
|
||||
const HorizontalPadding(),
|
||||
originalImage.visibility.type == VisibilityType.public
|
||||
? const Icon(Icons.public)
|
||||
: const Icon(Icons.lock),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(),
|
||||
LoginAwareCachedNetworkImage(imageUrl: originalImage.thumbnailUrl),
|
||||
const VerticalPadding(),
|
||||
TextField(
|
||||
controller: altTextController,
|
||||
maxLines: 10,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'ALT Text',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: const BorderSide(),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildError(ExecError error) {
|
||||
return [Text('Error loading image: $error')];
|
||||
}
|
||||
}
|
|
@ -26,6 +26,8 @@ class PostScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _PostScreenState extends State<PostScreen> {
|
||||
bool firstDraw = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(const Duration(milliseconds: 500), () async {
|
||||
|
@ -58,7 +60,10 @@ class _PostScreenState extends State<PostScreen> {
|
|||
isRoot: true,
|
||||
),
|
||||
),
|
||||
onError: (error) => Text('Error getting post: $error'));
|
||||
onError: (error) => Text(firstDraw || nss.timelineLoadingStatus.value
|
||||
? 'Attempting to load post'
|
||||
: 'Error getting post: $error'));
|
||||
firstDraw = true;
|
||||
return Scaffold(
|
||||
appBar: StandardAppBar.build(context, 'View Post', actions: []),
|
||||
body: Padding(
|
||||
|
|
|
@ -12,7 +12,7 @@ class GalleryService extends ChangeNotifier {
|
|||
static const IMAGES_PER_PAGE = 50;
|
||||
final _galleries = <String, GalleryData>{};
|
||||
final _galleryPages = <String, List<PagingData>>{};
|
||||
final _images = <String, Set<ImageEntry>>{};
|
||||
final _images = <String, List<ImageEntry>>{};
|
||||
var _loaded = false;
|
||||
|
||||
final Profile profile;
|
||||
|
@ -56,6 +56,11 @@ class GalleryService extends ChangeNotifier {
|
|||
return result.errorCast();
|
||||
}
|
||||
|
||||
final galleriesReturned = result.value.map((g) => g.name).toList();
|
||||
_galleries.clear();
|
||||
_galleryPages.removeWhere((key, value) => !galleriesReturned.contains(key));
|
||||
_images.removeWhere((key, value) => !galleriesReturned.contains(key));
|
||||
|
||||
for (final gallery in result.value) {
|
||||
_galleries[gallery.name] = gallery;
|
||||
}
|
||||
|
@ -83,6 +88,21 @@ class GalleryService extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
FutureResult<GalleryData, ExecError> renameGallery(
|
||||
GalleryData gallery, String newName) async {
|
||||
if (!_galleries.containsKey(gallery.name)) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Unknown gallery: ${gallery.name}');
|
||||
}
|
||||
final result = await GalleryClient(profile)
|
||||
.renameGallery(gallery.name, newName)
|
||||
.transform((_) => gallery.copy(name: newName))
|
||||
.withResultAsync((_) async => await updateGalleries());
|
||||
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
//TODO Paging
|
||||
FutureResult<List<ImageEntry>, ExecError> updateGalleryImageList(
|
||||
{required String galleryName,
|
||||
|
@ -92,11 +112,13 @@ class GalleryService extends ChangeNotifier {
|
|||
if (pages.isEmpty) {
|
||||
pages.add(PagingData(offset: 0, limit: IMAGES_PER_PAGE));
|
||||
} else if (withNextPage) {
|
||||
final offset = pages.last.offset! + 1;
|
||||
final offset = pages.last.offset! + IMAGES_PER_PAGE;
|
||||
pages.add(PagingData(offset: offset, limit: IMAGES_PER_PAGE));
|
||||
}
|
||||
|
||||
final imageSet = _images.putIfAbsent(galleryName, () => {});
|
||||
final imageSet = nextPageOnly
|
||||
? _images.putIfAbsent(galleryName, () => []).toSet()
|
||||
: <ImageEntry>{};
|
||||
|
||||
final pagesToUse = nextPageOnly ? [pages.last] : pages;
|
||||
for (final page in pagesToUse) {
|
||||
|
@ -108,7 +130,76 @@ class GalleryService extends ChangeNotifier {
|
|||
|
||||
imageSet.addAll(result.value);
|
||||
}
|
||||
|
||||
_images[galleryName] = imageSet.toList();
|
||||
notifyListeners();
|
||||
return Result.ok(imageSet.toList(growable: false));
|
||||
}
|
||||
|
||||
Result<ImageEntry, ExecError> getImage(String galleryName, String id) {
|
||||
if (!_images.containsKey(galleryName)) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Image gallery $galleryName not known.',
|
||||
);
|
||||
}
|
||||
|
||||
final potentialImages =
|
||||
_images[galleryName]?.where((i) => i.id == id).toList() ?? [];
|
||||
|
||||
if (potentialImages.isEmpty) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Image $id not found in gallery $galleryName',
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(potentialImages.first);
|
||||
}
|
||||
|
||||
FutureResult<ImageEntry, ExecError> updateImage(ImageEntry image) async {
|
||||
final images = _images[image.album];
|
||||
if (images == null) {
|
||||
buildErrorResult(
|
||||
type: ErrorType.notFound, message: 'Album not found ${image.album}');
|
||||
}
|
||||
|
||||
final index = _images[image.album]!.indexOf(image);
|
||||
if (index < 0) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Image ${image.id} does not exist for ${image.album}');
|
||||
}
|
||||
final result =
|
||||
await ImageClient(profile).editImageData(image).withResult((_) {
|
||||
images!.removeAt(index);
|
||||
images.insert(index, image);
|
||||
});
|
||||
notifyListeners();
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<ImageEntry, ExecError> deleteImage(ImageEntry image) async {
|
||||
final images = _images[image.album];
|
||||
if (images == null) {
|
||||
buildErrorResult(
|
||||
type: ErrorType.notFound, message: 'Album not found ${image.album}');
|
||||
}
|
||||
|
||||
final index = _images[image.album]!.indexOf(image);
|
||||
if (index < 0) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Image ${image.id} does not exist for ${image.album}');
|
||||
}
|
||||
final result = await ImageClient(profile)
|
||||
.deleteImage(image)
|
||||
.withResultAsync((_) async {
|
||||
images!.removeAt(index);
|
||||
await updateGalleries();
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
return result.execErrorCast();
|
||||
}
|
||||
}
|
||||
|
|
64
pubspec.lock
64
pubspec.lock
|
@ -269,10 +269,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "4fb13cf762bd84c4739eb7a5d8230f41493a749728dd2666ca259f7e4462eb2e"
|
||||
sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.1"
|
||||
version: "8.2.2"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -391,10 +391,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf
|
||||
sha256: "8ffe990dac54a4a5492747added38571a5ab474c8e5d196809ea08849c69b1bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
version: "2.0.13"
|
||||
flutter_portal:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -545,10 +545,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "4d141302e68dcdec0949ac798b5a8cf307eb6f79c70830ef1a2aec9b31827c28"
|
||||
sha256: bd7e671d26fd39c78cba82070fa34ef1f830b0e7ed1aeebccabc6561302a7ee5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.8"
|
||||
version: "6.5.9"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -593,10 +593,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227"
|
||||
sha256: "73964e3609fb96e01e69b0924b939967c556e46c7ff05db2ea9e31019000f4ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.15"
|
||||
version: "4.0.16"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -609,10 +609,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "1ea6870350f56af8dab716459bd9d5dc76947e29e07a2ba1d0c172eaaf4f269c"
|
||||
sha256: "7d2b25e0aec884495d782bb60cd09f779a92370035374c5ec622d09782ac68fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.6+7"
|
||||
version: "0.8.6+10"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -713,18 +713,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit
|
||||
sha256: "47292aae58bb0b118b7097d3aba1db86807b5ea8ecf056f1a2493663b801ff29"
|
||||
sha256: d9a32b3f6eafdfbba6aff2e37045a3a80009b6dfbdeec638d51d85e8b254a6a2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.6"
|
||||
version: "0.0.7+1"
|
||||
media_kit_libs_android_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: "25bf160a477628e5c39ee7c73978ba03a910e0a9d9500d2f4454346d2aef246d"
|
||||
sha256: b80aad84e038cdb641b941cefc2a7e9366881d19a14292f613c32ae6ec810e40
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.0.3"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -745,10 +745,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_macos_video
|
||||
sha256: ab1cbdf51400e30a9087bd7d6e10c6130d17296e76313fedd0ef0c57dae8c0f4
|
||||
sha256: c4d3706af5a67bf194d8208eef5cf29ea79e64931ea9bcd50f665b9a438f8416
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -769,10 +769,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_video
|
||||
sha256: c6db00eded195c1a8d91fc39d74f706f4db125ba03f0fe4bc9b33d46c6d133cc
|
||||
sha256: "634872ca85575e5756c466af04d1adbdb5178315d7f801901b8c731fae314c66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.8"
|
||||
version: "0.0.9"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -873,10 +873,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a
|
||||
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.25"
|
||||
version: "2.0.27"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1018,10 +1018,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: scrollable_positioned_list
|
||||
sha256: ca7fcaa743db712d4f7b1580526f494d0093c77a721a65705ee51fbeac7a2bd3
|
||||
sha256: "45806e0d64aa9dcbf4ced336eabff766dd7ba734014fd71c89bc08241c02bfc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
version: "0.3.6"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1034,10 +1034,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46"
|
||||
sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1119,18 +1119,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e7dfb6482d5d02b661d0b2399efa72b98909e5aa7b8336e1fb37e226264ade00
|
||||
sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.7"
|
||||
version: "2.2.8"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "220831bf0bd5333ff2445eee35ec131553b804e6b5d47a4a37ca6f5eb66e282c"
|
||||
sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.4.5"
|
||||
sqlite3:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1247,10 +1247,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411
|
||||
sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.27"
|
||||
version: "6.0.31"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1327,10 +1327,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: a592048a711d5739d9cea2255d425779f138d41095b9149bda60ce4bc1af8871
|
||||
sha256: "318c83a6329032eee2a90eafedf88da94dd0cd8bb2c2d0d5117764da25c697ff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.4.6"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
Ładowanie…
Reference in New Issue