Add preliminary gallery browser screen and gallery screen

codemagic-setup
Hank Grabowski 2022-12-13 21:06:10 -05:00
rodzic 3da3762324
commit aa8758c0c0
11 zmienionych plików z 420 dodań i 19 usunięć

Wyświetl plik

@ -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),

Wyświetl plik

@ -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';

Wyświetl plik

@ -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>(),
),

Wyświetl plik

@ -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});
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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,
);
}
}

Wyświetl plik

@ -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,
);
}
}

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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));
}
}