kopia lustrzana https://gitlab.com/mysocialportal/relatica
Add preliminary gallery browser screen and gallery screen
rodzic
3da3762324
commit
aa8758c0c0
|
@ -10,6 +10,7 @@ enum NavBarButtons {
|
|||
timelines,
|
||||
notifications,
|
||||
messages,
|
||||
gallery,
|
||||
contacts,
|
||||
profile,
|
||||
}
|
||||
|
@ -49,6 +50,9 @@ class AppBottomNavBar extends StatelessWidget {
|
|||
case NavBarButtons.profile:
|
||||
context.pushNamed(ScreenPaths.profile);
|
||||
break;
|
||||
case NavBarButtons.gallery:
|
||||
context.pushNamed(ScreenPaths.gallery);
|
||||
break;
|
||||
}
|
||||
},
|
||||
type: BottomNavigationBarType.fixed,
|
||||
|
@ -65,12 +69,14 @@ class AppBottomNavBar extends StatelessWidget {
|
|||
return 0;
|
||||
case NavBarButtons.notifications:
|
||||
return 1;
|
||||
case NavBarButtons.messages:
|
||||
case NavBarButtons.gallery:
|
||||
return 2;
|
||||
case NavBarButtons.contacts:
|
||||
case NavBarButtons.messages:
|
||||
return 3;
|
||||
case NavBarButtons.profile:
|
||||
case NavBarButtons.contacts:
|
||||
return 4;
|
||||
case NavBarButtons.profile:
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,14 +90,18 @@ class AppBottomNavBar extends StatelessWidget {
|
|||
}
|
||||
|
||||
if (index == 2) {
|
||||
return NavBarButtons.messages;
|
||||
return NavBarButtons.gallery;
|
||||
}
|
||||
|
||||
if (index == 3) {
|
||||
return NavBarButtons.contacts;
|
||||
return NavBarButtons.messages;
|
||||
}
|
||||
|
||||
if (index == 4) {
|
||||
return NavBarButtons.contacts;
|
||||
}
|
||||
|
||||
if (index == 5) {
|
||||
return NavBarButtons.profile;
|
||||
}
|
||||
|
||||
|
@ -114,6 +124,11 @@ class AppBottomNavBar extends StatelessWidget {
|
|||
? Icons.notifications_active
|
||||
: Icons.notifications),
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
label: 'Gallery',
|
||||
icon: Icon(Icons.photo_library_outlined),
|
||||
activeIcon: Icon(Icons.photo_library),
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
label: 'Messages',
|
||||
icon: Icon(Icons.messenger_outline),
|
||||
|
|
|
@ -8,10 +8,14 @@ import 'models/TimelineIdentifiers.dart';
|
|||
import 'models/connection.dart';
|
||||
import 'models/credentials.dart';
|
||||
import 'models/exec_error.dart';
|
||||
import 'models/gallery_data.dart';
|
||||
import 'models/group_data.dart';
|
||||
import 'models/image_entry.dart';
|
||||
import 'models/timeline_entry.dart';
|
||||
import 'models/user_notification.dart';
|
||||
import 'serializers/friendica/connection_friendica_extensions.dart';
|
||||
import 'serializers/friendica/gallery_data_friendica_extensions.dart';
|
||||
import 'serializers/friendica/image_entry_friendica_extensions.dart';
|
||||
import 'serializers/mastodon/group_data_mastodon_extensions.dart';
|
||||
import 'serializers/mastodon/notification_mastodon_extension.dart';
|
||||
import 'serializers/mastodon/timeline_entry_mastodon_extensions.dart';
|
||||
|
@ -68,6 +72,34 @@ class FriendicaClient {
|
|||
return response.mapValue((value) => true);
|
||||
}
|
||||
|
||||
FutureResult<List<GalleryData>, ExecError> getGalleryData() async {
|
||||
_logger.finest(() => 'Getting gallery data');
|
||||
final url = 'https://$serverName/api/friendica/photoalbums';
|
||||
final request = Uri.parse(url);
|
||||
return (await _getApiListRequest(request).andThenSuccessAsync(
|
||||
(albumsJson) async => albumsJson
|
||||
.map((json) => GalleryDataFriendicaExtensions.fromJson(json))
|
||||
.toList()))
|
||||
.mapError((error) => error is ExecError
|
||||
? error
|
||||
: ExecError(type: ErrorType.localError, message: error.toString()));
|
||||
}
|
||||
|
||||
FutureResult<List<ImageEntry>, ExecError> getGalleryImages(
|
||||
String galleryName) async {
|
||||
_logger.finest(() => 'Getting gallery data');
|
||||
final url =
|
||||
'https://$serverName/api/friendica/photoalbum?album=$galleryName';
|
||||
final request = Uri.parse(url);
|
||||
return (await _getApiListRequest(request).andThenSuccessAsync(
|
||||
(imagesJson) async => imagesJson
|
||||
.map((json) => ImageEntryFriendicaExtension.fromJson(json))
|
||||
.toList()))
|
||||
.mapError((error) => error is ExecError
|
||||
? error
|
||||
: ExecError(type: ErrorType.localError, message: error.toString()));
|
||||
}
|
||||
|
||||
FutureResult<List<GroupData>, ExecError> getGroups() async {
|
||||
_logger.finest(() => 'Getting group (Mastodon List) data');
|
||||
final url = 'https://$serverName/api/v1/lists';
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'routes.dart';
|
|||
import 'services/auth_service.dart';
|
||||
import 'services/connections_manager.dart';
|
||||
import 'services/entry_manager_service.dart';
|
||||
import 'services/gallery_service.dart';
|
||||
import 'services/notifications_manager.dart';
|
||||
import 'services/secrets_service.dart';
|
||||
import 'services/timeline_manager.dart';
|
||||
|
@ -39,6 +40,7 @@ void main() async {
|
|||
final entryManagerService = EntryManagerService();
|
||||
final timelineManager = TimelineManager();
|
||||
getIt.registerLazySingleton<ConnectionsManager>(() => ConnectionsManager());
|
||||
getIt.registerLazySingleton<GalleryService>(() => GalleryService());
|
||||
getIt.registerSingleton<EntryManagerService>(entryManagerService);
|
||||
getIt.registerSingleton<SecretsService>(secretsService);
|
||||
getIt.registerSingleton<AuthService>(authService);
|
||||
|
@ -83,6 +85,10 @@ class App extends StatelessWidget {
|
|||
create: (_) => getIt<EntryManagerService>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<GalleryService>(
|
||||
create: (_) => getIt<GalleryService>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<TimelineManager>(
|
||||
create: (_) => getIt<TimelineManager>(),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
class GalleryData {
|
||||
final int count;
|
||||
final String name;
|
||||
final DateTime created;
|
||||
|
||||
GalleryData({required this.count, required this.name, required this.created});
|
||||
}
|
|
@ -1,19 +1,29 @@
|
|||
class ImageEntry {
|
||||
final String postId;
|
||||
final String localFilename;
|
||||
final String url;
|
||||
final String id;
|
||||
final String album;
|
||||
final String filename;
|
||||
final String description;
|
||||
final String thumbnailUrl;
|
||||
final DateTime created;
|
||||
final int height;
|
||||
final int width;
|
||||
|
||||
ImageEntry(
|
||||
{required this.postId, required this.localFilename, required this.url});
|
||||
ImageEntry({
|
||||
required this.id,
|
||||
required this.album,
|
||||
required this.filename,
|
||||
required this.description,
|
||||
required this.thumbnailUrl,
|
||||
required this.created,
|
||||
required this.height,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
ImageEntry.fromJson(Map<String, dynamic> json)
|
||||
: postId = json['postId'] ?? '',
|
||||
localFilename = json['localFilename'] ?? '',
|
||||
url = json['url'] ?? '';
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ImageEntry && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'postId': postId,
|
||||
'localFilename': localFilename,
|
||||
'url': url,
|
||||
};
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:go_router/go_router.dart';
|
|||
|
||||
import 'globals.dart';
|
||||
import 'screens/editor.dart';
|
||||
import 'screens/gallery_browsers_screen.dart';
|
||||
import 'screens/gallery_screen.dart';
|
||||
import 'screens/home.dart';
|
||||
import 'screens/notifications_screen.dart';
|
||||
import 'screens/post_screen.dart';
|
||||
|
@ -15,6 +17,7 @@ import 'services/auth_service.dart';
|
|||
class ScreenPaths {
|
||||
static String splash = '/splash';
|
||||
static String timelines = '/';
|
||||
static String gallery = '/gallery';
|
||||
static String profile = '/profile';
|
||||
static String notifications = '/notifications';
|
||||
static String signin = '/signin';
|
||||
|
@ -73,6 +76,21 @@ final appRouter = GoRouter(
|
|||
child: ProfileScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: ScreenPaths.gallery,
|
||||
name: ScreenPaths.gallery,
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: GalleryBrowsersScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'show/:name',
|
||||
builder: (context, state) => GalleryScreen(
|
||||
galleryName: state.params['name']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: ScreenPaths.notifications,
|
||||
name: ScreenPaths.notifications,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/app_bottom_nav_bar.dart';
|
||||
import '../controls/padding.dart';
|
||||
import '../services/gallery_service.dart';
|
||||
|
||||
class GalleryBrowsersScreen extends StatelessWidget {
|
||||
static final _logger = Logger('$GalleryBrowsersScreen');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building');
|
||||
final service = context.watch<GalleryService>();
|
||||
return Scaffold(
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
print('Refresh gallery list');
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await service.updateGalleries();
|
||||
},
|
||||
child: buildBody(context, service)),
|
||||
),
|
||||
bottomNavigationBar: AppBottomNavBar(
|
||||
currentButton: NavBarButtons.gallery,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context, GalleryService service) {
|
||||
final galleries = service.getGalleries();
|
||||
|
||||
if (galleries.isEmpty && service.loaded) {
|
||||
return const SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Text('No Galleries'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (galleries.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: const [
|
||||
Text('Loading galleries'),
|
||||
VerticalPadding(),
|
||||
CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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.caption,
|
||||
),
|
||||
trailing: Text('${gallery.count} Images'),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const Divider();
|
||||
},
|
||||
itemCount: galleries.length,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/padding.dart';
|
||||
import '../serializers/friendica/image_entry_friendica_extensions.dart';
|
||||
import '../services/gallery_service.dart';
|
||||
import 'image_viewer_screen.dart';
|
||||
|
||||
class GalleryScreen extends StatelessWidget {
|
||||
static const thumbnailDimension = 100.0;
|
||||
static final _logger = Logger('$GalleryScreen');
|
||||
final String galleryName;
|
||||
|
||||
const GalleryScreen({super.key, required this.galleryName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building');
|
||||
final service = context.watch<GalleryService>();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(galleryName),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
print('Refresh $galleryName image list');
|
||||
},
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await service.updateGalleryImageList(galleryName);
|
||||
},
|
||||
child: buildBody(context, service)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context, GalleryService service) {
|
||||
final imageResult = service.getGalleryImageList(galleryName);
|
||||
if (imageResult.isFailure) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Text('Error getting images for gallery: ${imageResult.error}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final images = imageResult.value;
|
||||
if (images.isEmpty && service.loaded) {
|
||||
return const SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Text('No images'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (images.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: const [
|
||||
Text('Loading images'),
|
||||
VerticalPadding(),
|
||||
CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
final image = images[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return ImageViewerScreen(attachment: image.toMediaAttachment());
|
||||
}));
|
||||
},
|
||||
child: ListTile(
|
||||
leading: CachedNetworkImage(
|
||||
width: thumbnailDimension,
|
||||
height: thumbnailDimension,
|
||||
imageUrl: image.thumbnailUrl,
|
||||
),
|
||||
title: Text(image.filename),
|
||||
subtitle: Text(
|
||||
image.description,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
trailing: Text(image.created.toString()),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const Divider();
|
||||
},
|
||||
itemCount: images.length,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import '../../models/gallery_data.dart';
|
||||
|
||||
extension GalleryDataFriendicaExtensions on GalleryData {
|
||||
static GalleryData fromJson(Map<String, dynamic> json) => GalleryData(
|
||||
count: json['count'] ?? -1,
|
||||
name: json['name'] ?? 'Unknown',
|
||||
created: DateTime.tryParse(json['created']) ?? DateTime(0));
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import '../../models/attachment_media_type_enum.dart';
|
||||
import '../../models/image_entry.dart';
|
||||
import '../../models/media_attachment.dart';
|
||||
|
||||
extension ImageEntryFriendicaExtension on ImageEntry {
|
||||
static ImageEntry fromJson(Map<String, dynamic> json) => ImageEntry(
|
||||
id: json['id'],
|
||||
album: json['album'],
|
||||
filename: json['filename'],
|
||||
description: json['desc'],
|
||||
thumbnailUrl: json['thumb'],
|
||||
created: DateTime.tryParse(json['created']) ?? DateTime(0),
|
||||
height: json['height'],
|
||||
width: json['width'],
|
||||
);
|
||||
|
||||
MediaAttachment toMediaAttachment() {
|
||||
final thumbUri = Uri.parse(thumbnailUrl);
|
||||
final extension = thumbUri.pathSegments.last.split('.').last;
|
||||
final newFileName = '$id-0.$extension';
|
||||
final fullFileUri = Uri.https(thumbUri.authority, '/photo/$newFileName');
|
||||
return MediaAttachment(
|
||||
uri: fullFileUri,
|
||||
creationTimestamp: created.millisecondsSinceEpoch,
|
||||
metadata: {},
|
||||
thumbnailUri: thumbUri,
|
||||
title: filename,
|
||||
explicitType: AttachmentMediaType.image,
|
||||
description: description);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/gallery_data.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import 'auth_service.dart';
|
||||
|
||||
class GalleryService extends ChangeNotifier {
|
||||
static final _logger = Logger('$GalleryService');
|
||||
final _galleries = <String, GalleryData>{};
|
||||
final _images = <String, Set<ImageEntry>>{};
|
||||
var _loaded = false;
|
||||
|
||||
bool get loaded => _loaded;
|
||||
|
||||
List<GalleryData> getGalleries() {
|
||||
if (_galleries.isEmpty) {
|
||||
updateGalleries();
|
||||
}
|
||||
|
||||
return _galleries.values.toList(growable: false);
|
||||
}
|
||||
|
||||
FutureResult<List<GalleryData>, ExecError> updateGalleries() async {
|
||||
final auth = getIt<AuthService>();
|
||||
final clientResult = auth.currentClient;
|
||||
if (clientResult.isFailure) {
|
||||
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
||||
return clientResult.errorCast();
|
||||
}
|
||||
|
||||
final client = clientResult.value;
|
||||
final result = await client.getGalleryData();
|
||||
if (result.isFailure) {
|
||||
return result.errorCast();
|
||||
}
|
||||
|
||||
for (final gallery in result.value) {
|
||||
_galleries[gallery.name] = gallery;
|
||||
}
|
||||
|
||||
_loaded = true;
|
||||
notifyListeners();
|
||||
return Result.ok(_galleries.values.toList(growable: false));
|
||||
}
|
||||
|
||||
Result<List<ImageEntry>, ExecError> getGalleryImageList(String galleryName) {
|
||||
if (!_galleries.containsKey(galleryName)) {
|
||||
return Result.error(
|
||||
ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: 'Unknown Gallery: $galleryName',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_images.containsKey(galleryName)) {
|
||||
updateGalleryImageList(galleryName);
|
||||
return Result.ok([]);
|
||||
} else {
|
||||
return Result.ok(_images[galleryName]!.toList(growable: false));
|
||||
}
|
||||
}
|
||||
|
||||
//TODO Paging
|
||||
FutureResult<List<ImageEntry>, ExecError> updateGalleryImageList(
|
||||
String galleryName) async {
|
||||
final auth = getIt<AuthService>();
|
||||
final clientResult = auth.currentClient;
|
||||
if (clientResult.isFailure) {
|
||||
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
||||
return clientResult.errorCast();
|
||||
}
|
||||
|
||||
final client = clientResult.value;
|
||||
final result = await client.getGalleryImages(galleryName);
|
||||
if (result.isFailure) {
|
||||
return result.errorCast();
|
||||
}
|
||||
|
||||
final imageSet = _images.putIfAbsent(galleryName, () => {});
|
||||
imageSet.addAll(result.value);
|
||||
|
||||
notifyListeners();
|
||||
return Result.ok(imageSet.toList(growable: false));
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue