Merge pull request #5 from sBubshait/feature/posts

Posts
This commit is contained in:
Saleh Bubshait 2025-07-23 15:11:50 +03:00 committed by GitHub
commit 92026f71e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 2350 additions and 438 deletions

View File

@ -4,9 +4,12 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class OpenApiConfig {
@ -17,12 +20,17 @@ public class OpenApiConfig {
.title("Wesal API")
.description("Social media application API")
.version("1.0.0"))
.servers(List.of(
new Server().url("http://localhost:8080").description("Development server"),
new Server().url("https://api.wesal.online").description("Production server")
))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
.bearerFormat("JWT")
.description("Enter JWT token (without 'Bearer ' prefix)")));
}
}

View File

@ -67,7 +67,7 @@ public class SecurityConfig {
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/", "/login", "/swagger-ui/**", "/v3/api-docs/**", "/docs/**", "/docs").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)

View File

@ -0,0 +1,187 @@
package online.wesal.wesal.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import online.wesal.wesal.dto.ApiResponse;
import online.wesal.wesal.dto.PostCreateRequestDTO;
import online.wesal.wesal.dto.PostLikeRequestDTO;
import online.wesal.wesal.dto.PostResponseDTO;
import online.wesal.wesal.entity.Post;
import online.wesal.wesal.service.PostService;
import online.wesal.wesal.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/posts")
@CrossOrigin(origins = "*")
@Tag(name = "Posts", description = "Post management endpoints")
public class PostController {
@Autowired
private PostService postService;
@Autowired
private UserService userService;
@PostMapping(value = "/create", consumes = "application/json", produces = "application/json")
@Operation(summary = "Create post", description = "Create a new post")
public ResponseEntity<ApiResponse<PostResponseDTO>> createPost(
@Valid @RequestBody PostCreateRequestDTO request,
BindingResult bindingResult,
Authentication authentication) {
if (bindingResult.hasErrors()) {
String errorMessage = bindingResult.getFieldErrors().stream()
.map(error -> {
String field = error.getField();
if ("body".equals(field)) return "Post body is required and cannot exceed 2000 characters";
return error.getDefaultMessage();
})
.findFirst()
.orElse("Invalid input");
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
}
try {
PostResponseDTO response = postService.createPostWithResponse(request.getBody());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
if (e.getMessage().contains("User not found")) {
message = "Authentication error. Please log in again.";
} else {
message = "Something went wrong.. We're sorry but try again later";
}
return ResponseEntity.badRequest().body(ApiResponse.error(message));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
@GetMapping("/all")
@Operation(summary = "Get all recent posts", description = "Get all posts from the last 7 days, ordered by creation date (latest first)")
public ResponseEntity<ApiResponse<List<PostResponseDTO>>> getAllRecentPosts() {
try {
List<PostResponseDTO> response = postService.getAllRecentPosts();
return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
@GetMapping("/user")
@Operation(summary = "Get user posts", description = "Get all posts by a specific user, ordered by creation date (latest first). If no id provided, returns authenticated user's posts.")
public ResponseEntity<ApiResponse<List<PostResponseDTO>>> getUserPosts(
@RequestParam(required = false) Long id,
Authentication authentication) {
try {
Long targetUserId;
if (id == null) {
// Use authenticated user's ID when no id is provided
String userEmail = authentication.getName();
targetUserId = userService.getCurrentUser().getId();
} else {
if (id <= 0) {
return ResponseEntity.badRequest().body(ApiResponse.error("Valid user ID is required"));
}
targetUserId = id;
}
List<PostResponseDTO> response = postService.getUserPosts(targetUserId);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
if (e.getMessage().contains("User not found")) {
message = "User not found";
} else {
message = "Something went wrong.. We're sorry but try again later";
}
return ResponseEntity.badRequest().body(ApiResponse.error(message));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
@PostMapping(value = "/like", consumes = "application/json", produces = "application/json")
@Operation(summary = "Like post", description = "Like a post. Returns the updated post with new like count.")
public ResponseEntity<ApiResponse<PostResponseDTO>> likePost(
@Valid @RequestBody PostLikeRequestDTO request,
BindingResult bindingResult,
Authentication authentication) {
if (bindingResult.hasErrors()) {
String errorMessage = bindingResult.getFieldErrors().stream()
.map(error -> {
String field = error.getField();
if ("postId".equals(field)) return "Valid post ID is required";
return error.getDefaultMessage();
})
.findFirst()
.orElse("Invalid input");
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
}
try {
PostResponseDTO response = postService.likePost(request.getPostId());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
if (e.getMessage().contains("Post not found")) {
message = "Post not found";
} else if (e.getMessage().contains("User not found")) {
message = "Authentication error. Please log in again.";
} else {
message = "Something went wrong.. We're sorry but try again later";
}
return ResponseEntity.badRequest().body(ApiResponse.error(message));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
@PostMapping(value = "/unlike", consumes = "application/json", produces = "application/json")
@Operation(summary = "Unlike post", description = "Unlike a post. Returns the updated post with new like count.")
public ResponseEntity<ApiResponse<PostResponseDTO>> unlikePost(
@Valid @RequestBody PostLikeRequestDTO request,
BindingResult bindingResult,
Authentication authentication) {
if (bindingResult.hasErrors()) {
String errorMessage = bindingResult.getFieldErrors().stream()
.map(error -> {
String field = error.getField();
if ("postId".equals(field)) return "Valid post ID is required";
return error.getDefaultMessage();
})
.findFirst()
.orElse("Invalid input");
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
}
try {
PostResponseDTO response = postService.unlikePost(request.getPostId());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) {
String message;
if (e.getMessage().contains("Post not found")) {
message = "Post not found";
} else if (e.getMessage().contains("User not found")) {
message = "Authentication error. Please log in again.";
} else {
message = "Something went wrong.. We're sorry but try again later";
}
return ResponseEntity.badRequest().body(ApiResponse.error(message));
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
}
}
}

View File

@ -11,7 +11,7 @@ public class CreateUserRequest {
private String email;
@NotBlank
@Size(min = 8)
@Size(min = 6)
private String password;
@NotBlank

View File

@ -0,0 +1,32 @@
package online.wesal.wesal.dto;
import online.wesal.wesal.entity.User;
public class CreatorDTO {
private String id;
private String displayName;
public CreatorDTO() {}
public CreatorDTO(User user) {
this.id = String.valueOf(user.getId());
this.displayName = user.getDisplayName();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}

View File

@ -0,0 +1,25 @@
package online.wesal.wesal.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class PostCreateRequestDTO {
@NotBlank(message = "Post body cannot be empty")
@Size(max = 2000, message = "Post body cannot exceed 2000 characters")
private String body;
public PostCreateRequestDTO() {}
public PostCreateRequestDTO(String body) {
this.body = body;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}

View File

@ -0,0 +1,25 @@
package online.wesal.wesal.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
public class PostLikeRequestDTO {
@NotNull(message = "Post ID is required")
@Positive(message = "Post ID must be a positive number")
private Long postId;
public PostLikeRequestDTO() {}
public PostLikeRequestDTO(Long postId) {
this.postId = postId;
}
public Long getPostId() {
return postId;
}
public void setPostId(Long postId) {
this.postId = postId;
}
}

View File

@ -0,0 +1,94 @@
package online.wesal.wesal.dto;
import online.wesal.wesal.entity.Post;
import online.wesal.wesal.entity.User;
import java.time.LocalDateTime;
public class PostResponseDTO {
private String id;
private CreatorDTO creator;
private String body;
private String likes;
private String comments;
private boolean liked;
private LocalDateTime creationDate;
public PostResponseDTO() {}
public PostResponseDTO(Post post, User creator) {
this.id = String.valueOf(post.getId());
this.creator = new CreatorDTO(creator);
this.body = post.getBody();
this.likes = String.valueOf(post.getLikes());
this.comments = String.valueOf(post.getComments());
this.liked = false; // Default value, will be set by service
this.creationDate = post.getCreationDate();
}
public PostResponseDTO(Post post, User creator, boolean liked) {
this.id = String.valueOf(post.getId());
this.creator = new CreatorDTO(creator);
this.body = post.getBody();
this.likes = String.valueOf(post.getLikes());
this.comments = String.valueOf(post.getComments());
this.liked = liked;
this.creationDate = post.getCreationDate();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public CreatorDTO getCreator() {
return creator;
}
public void setCreator(CreatorDTO creator) {
this.creator = creator;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getLikes() {
return likes;
}
public void setLikes(String likes) {
this.likes = likes;
}
public String getComments() {
return comments;
}
public void setComments(String comments) {
this.comments = comments;
}
public boolean isLiked() {
return liked;
}
public void setLiked(boolean liked) {
this.liked = liked;
}
public LocalDateTime getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDateTime creationDate) {
this.creationDate = creationDate;
}
}

View File

@ -10,7 +10,7 @@ public class UpdateUserRequest {
private String avatar;
@Size(min = 8, message = "Password must be at least 8 characters long")
@Size(min = 6, message = "Password must be at least 8 characters long")
private String password;
public UpdateUserRequest() {}

View File

@ -0,0 +1,85 @@
package online.wesal.wesal.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long creatorId;
@Column(nullable = false, length = 2000)
@NotBlank
private String body;
@Column(nullable = false)
private int likes = 0;
@Column(nullable = false)
private int comments = 0;
@Column(nullable = false)
private LocalDateTime creationDate = LocalDateTime.now();
public Post() {}
public Post(Long creatorId, String body) {
this.creatorId = creatorId;
this.body = body;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCreatorId() {
return creatorId;
}
public void setCreatorId(Long creatorId) {
this.creatorId = creatorId;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public int getLikes() {
return likes;
}
public void setLikes(int likes) {
this.likes = likes;
}
public int getComments() {
return comments;
}
public void setComments(int comments) {
this.comments = comments;
}
public LocalDateTime getCreationDate() {
return creationDate;
}
public void setCreationDate(LocalDateTime creationDate) {
this.creationDate = creationDate;
}
}

View File

@ -0,0 +1,63 @@
package online.wesal.wesal.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "post_likes", uniqueConstraints = {
@UniqueConstraint(columnNames = {"post_id", "user_id"})
})
public class PostLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, name = "post_id")
private Long postId;
@Column(nullable = false, name = "user_id")
private Long userId;
@Column(nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
public PostLike() {}
public PostLike(Long postId, Long userId) {
this.postId = postId;
this.userId = userId;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getPostId() {
return postId;
}
public void setPostId(Long postId) {
this.postId = postId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@ -23,7 +23,7 @@ public class User {
@Column(nullable = false)
@NotBlank
@Size(min = 8)
@Size(min = 6)
private String password;
@Column(nullable = false)

View File

@ -0,0 +1,25 @@
package online.wesal.wesal.repository;
import online.wesal.wesal.entity.PostLike;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Repository
public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
Optional<PostLike> findByPostIdAndUserId(Long postId, Long userId);
boolean existsByPostIdAndUserId(Long postId, Long userId);
void deleteByPostIdAndUserId(Long postId, Long userId);
long countByPostId(Long postId);
@Query("SELECT pl.postId FROM PostLike pl WHERE pl.userId = :userId AND pl.postId IN :postIds")
Set<Long> findLikedPostIdsByUserIdAndPostIds(@Param("userId") Long userId, @Param("postIds") List<Long> postIds);
@Query("SELECT pl.postId FROM PostLike pl WHERE pl.userId = :userId")
Set<Long> findAllLikedPostIdsByUserId(@Param("userId") Long userId);
}

View File

@ -0,0 +1,20 @@
package online.wesal.wesal.repository;
import online.wesal.wesal.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByCreatorIdOrderByCreationDateDesc(Long creatorId);
@Query("SELECT p FROM Post p WHERE p.creationDate >= :sevenDaysAgo ORDER BY p.creationDate DESC")
List<Post> findAllPostsWithinLast7Days(@Param("sevenDaysAgo") LocalDateTime sevenDaysAgo);
List<Post> findAllByOrderByCreationDateDesc();
}

View File

@ -0,0 +1,158 @@
package online.wesal.wesal.service;
import online.wesal.wesal.dto.PostResponseDTO;
import online.wesal.wesal.entity.Post;
import online.wesal.wesal.entity.PostLike;
import online.wesal.wesal.entity.User;
import online.wesal.wesal.repository.PostLikeRepository;
import online.wesal.wesal.repository.PostRepository;
import online.wesal.wesal.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class PostService {
@Autowired
private PostRepository postRepository;
@Autowired
private PostLikeRepository postLikeRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
public Post createPost(String body) {
User currentUser = userService.getCurrentUser();
Post post = new Post(currentUser.getId(), body);
return postRepository.save(post);
}
public List<PostResponseDTO> getAllRecentPosts() {
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
List<Post> posts = postRepository.findAllPostsWithinLast7Days(sevenDaysAgo);
User currentUser = userService.getCurrentUser();
Long currentUserId = currentUser.getId();
// Get all unique creator IDs
List<Long> creatorIds = posts.stream()
.map(Post::getCreatorId)
.distinct()
.collect(Collectors.toList());
// Get all post IDs
List<Long> postIds = posts.stream()
.map(Post::getId)
.collect(Collectors.toList());
// Fetch creators in one query
Map<Long, User> creators = userRepository.findAllById(creatorIds).stream()
.collect(Collectors.toMap(User::getId, user -> user));
// Fetch user's likes for these posts in one query
Set<Long> likedPostIds = postLikeRepository.findLikedPostIdsByUserIdAndPostIds(currentUserId, postIds);
return posts.stream()
.map(post -> new PostResponseDTO(post, creators.get(post.getCreatorId()),
likedPostIds.contains(post.getId())))
.collect(Collectors.toList());
}
public List<PostResponseDTO> getUserPosts(Long userId) {
List<Post> posts = postRepository.findByCreatorIdOrderByCreationDateDesc(userId);
User creator = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
User currentUser = userService.getCurrentUser();
Long currentUserId = currentUser.getId();
// Get all post IDs
List<Long> postIds = posts.stream()
.map(Post::getId)
.collect(Collectors.toList());
// Fetch user's likes for these posts in one query
Set<Long> likedPostIds = postLikeRepository.findLikedPostIdsByUserIdAndPostIds(currentUserId, postIds);
return posts.stream()
.map(post -> new PostResponseDTO(post, creator, likedPostIds.contains(post.getId())))
.collect(Collectors.toList());
}
public PostResponseDTO createPostWithResponse(String body) {
User currentUser = userService.getCurrentUser();
Post post = new Post(currentUser.getId(), body);
post = postRepository.save(post);
return new PostResponseDTO(post, currentUser, false); // New post is not liked by default
}
@Transactional
public PostResponseDTO likePost(Long postId) {
User currentUser = userService.getCurrentUser();
Long userId = currentUser.getId();
// Check if post exists
Post post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("Post not found"));
// Check if user already liked this post
if (!postLikeRepository.existsByPostIdAndUserId(postId, userId)) {
// Add like record
PostLike postLike = new PostLike(postId, userId);
postLikeRepository.save(postLike);
// Update post likes count
post.setLikes(post.getLikes() + 1);
post = postRepository.save(post);
}
// Get creator for response
User creator = userRepository.findById(post.getCreatorId())
.orElseThrow(() -> new RuntimeException("Creator not found"));
// Check if user now likes this post
boolean isLiked = postLikeRepository.existsByPostIdAndUserId(postId, userId);
return new PostResponseDTO(post, creator, isLiked);
}
@Transactional
public PostResponseDTO unlikePost(Long postId) {
User currentUser = userService.getCurrentUser();
Long userId = currentUser.getId();
// Check if post exists
Post post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("Post not found"));
// Check if user has liked this post
if (postLikeRepository.existsByPostIdAndUserId(postId, userId)) {
// Remove like record
postLikeRepository.deleteByPostIdAndUserId(postId, userId);
// Update post likes count
post.setLikes(Math.max(0, post.getLikes() - 1));
post = postRepository.save(post);
}
// Get creator for response
User creator = userRepository.findById(post.getCreatorId())
.orElseThrow(() -> new RuntimeException("Creator not found"));
// Check if user now likes this post
boolean isLiked = postLikeRepository.existsByPostIdAndUserId(postId, userId);
return new PostResponseDTO(post, creator, isLiked);
}
}

View File

@ -19,3 +19,9 @@ spring:
server:
port: 8080
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /docs

View File

@ -1,5 +1,5 @@
class ApiConstants {
static const String baseUrl = 'https://api.wesal.online';
static const String baseUrl = 'http://localhost:8080';
// Auth endpoints
static const String loginEndpoint = '/login';
@ -13,4 +13,7 @@ class ApiConstants {
static const String getAllInvitationsEndpoint = '/invitations/all';
static const String acceptInvitationEndpoint = '/invitations/accept';
static const String createInvitationEndpoint = '/invitations/create';
// Post endpoints
static const String createPostEndpoint = '/posts/create';
}

View File

@ -0,0 +1,105 @@
class PostCreator {
final String id;
final String displayName;
PostCreator({
required this.id,
required this.displayName,
});
factory PostCreator.fromJson(Map<String, dynamic> json) {
return PostCreator(
id: json['id']?.toString() ?? '',
displayName: json['displayName'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'displayName': displayName,
};
}
}
class Post {
final String id;
final String creatorId;
final PostCreator creator;
final String body;
final int likes;
final int comments;
final DateTime creationDate;
final bool liked;
Post({
required this.id,
required this.creatorId,
required this.creator,
required this.body,
required this.likes,
required this.comments,
required this.creationDate,
this.liked = false,
});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id']?.toString() ?? '',
creatorId: json['creatorId']?.toString() ?? '',
creator: PostCreator.fromJson(json['creator'] ?? {}),
body: json['body'] ?? '',
likes: int.tryParse(json['likes']?.toString() ?? '0') ?? 0,
comments: int.tryParse(json['comments']?.toString() ?? '0') ?? 0,
creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()),
liked: json['liked'] == true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'creatorId': creatorId,
'creator': creator.toJson(),
'body': body,
'likes': likes,
'comments': comments,
'creationDate': creationDate.toIso8601String(),
'liked': liked,
};
}
Post copyWith({
String? id,
String? creatorId,
PostCreator? creator,
String? body,
int? likes,
int? comments,
DateTime? creationDate,
bool? liked,
}) {
return Post(
id: id ?? this.id,
creatorId: creatorId ?? this.creatorId,
creator: creator ?? this.creator,
body: body ?? this.body,
likes: likes ?? this.likes,
comments: comments ?? this.comments,
creationDate: creationDate ?? this.creationDate,
liked: liked ?? this.liked,
);
}
}
class CreatePostRequest {
final String body;
CreatePostRequest({required this.body});
Map<String, dynamic> toJson() {
return {
'body': body,
};
}
}

View File

@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import '../services/post_service.dart';
class CreatePostScreen extends StatefulWidget {
@override
_CreatePostScreenState createState() => _CreatePostScreenState();
}
class _CreatePostScreenState extends State<CreatePostScreen> {
final TextEditingController _bodyController = TextEditingController();
bool _isLoading = false;
final int _maxCharacters = 280;
@override
void dispose() {
_bodyController.dispose();
super.dispose();
}
Future<void> _createPost() async {
if (_bodyController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please write something before posting'),
backgroundColor: Colors.red,
),
);
return;
}
setState(() {
_isLoading = true;
});
try {
final result = await PostService.createPost(_bodyController.text.trim());
if (result['success']) {
Navigator.of(context).pop(true); // Return true to indicate success
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Post created successfully!'),
backgroundColor: Color(0xFF6A4C93),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result['message']),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to create post. Please try again.'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
final remainingChars = _maxCharacters - _bodyController.text.length;
final isOverLimit = remainingChars < 0;
return Scaffold(
appBar: AppBar(
title: Text('Create Post', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white,
foregroundColor: Color(0xFF6A4C93),
elevation: 0,
bottom: PreferredSize(
preferredSize: Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[200]),
),
actions: [
TextButton(
onPressed: _isLoading || isOverLimit || _bodyController.text.trim().isEmpty
? null
: _createPost,
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
),
)
: Text(
'Post',
style: TextStyle(
color: _bodyController.text.trim().isEmpty || isOverLimit
? Colors.grey
: Color(0xFF6A4C93),
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
SizedBox(width: 16),
],
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF32B0A5).withOpacity(0.05),
Color(0xFF4600B9).withOpacity(0.05),
],
),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"What's on your mind?",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF6A4C93),
),
),
SizedBox(height: 16),
TextField(
controller: _bodyController,
maxLines: 6,
decoration: InputDecoration(
hintText: 'Share your thoughts...',
hintStyle: TextStyle(color: Colors.grey[500]),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
style: TextStyle(
fontSize: 16,
height: 1.4,
color: Colors.black87,
),
onChanged: (text) {
setState(() {});
},
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Share your thoughts with the community',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
Text(
'$remainingChars',
style: TextStyle(
color: isOverLimit ? Colors.red : Colors.grey[600],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
if (isOverLimit)
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'Post is too long. Please keep it under $_maxCharacters characters.',
style: TextStyle(
color: Colors.red,
fontSize: 12,
),
),
),
],
),
),
),
SizedBox(height: 24),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Icon(
Icons.lightbulb_outline,
color: Color(0xFF6A4C93),
size: 20,
),
SizedBox(width: 12),
Expanded(
child: Text(
'Keep it friendly and respectful. Your post will be visible to everyone in the community.',
style: TextStyle(
color: Colors.grey[700],
fontSize: 13,
height: 1.3,
),
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart';
import '../create_post_screen.dart';
import '../../services/post_service.dart';
import '../../widgets/posts_list.dart';
class FeedPage extends StatefulWidget {
@override
@ -6,63 +9,8 @@ class FeedPage extends StatefulWidget {
}
class _FeedPageState extends State<FeedPage> {
final List<Map<String, dynamic>> mockPosts = [
{
'id': '1',
'user': {
'displayName': 'Abu Khalid (Aqeel)',
'avatar': 'A',
'avatar_color': Color(0xFF32B0A5),
},
'content': 'Free hasawi khalas dates! Drop by my office to grab some 😉',
'timestamp': '42 minutes ago',
'likes': 12,
'comments': 3,
'isLiked': false,
},
{
'id': '2',
'user': {
'displayName': 'Sarah Khalid',
'avatar': 'S',
'avatar_color': Color(0xFF4600B9),
},
'content':
'Alhamdulillah, I am happy to tell you I have been blessed with a baby ❤️',
'timestamp': '4 hours ago',
'likes': 28,
'comments': 7,
'isLiked': true,
},
{
'id': '3',
'user': {
'displayName': 'Omar Hassan',
'avatar': 'O',
'avatar_color': Color(0xFF6A4C93),
},
'content':
'The sunset view from my balcony tonight is absolutely breathtaking. Sometimes you just need to pause and appreciate the beauty around us.',
'timestamp': '1 day ago',
'likes': 45,
'comments': 12,
'isLiked': false,
},
{
'id': '4',
'user': {
'displayName': 'Fatima Al-Zahra',
'avatar': 'F',
'avatar_color': Color(0xFF32B0A5),
},
'content':
'Finished reading an incredible book today. "The Seven Habits of Highly Effective People" - highly recommend it to anyone looking for personal development!',
'timestamp': '2 days ago',
'likes': 19,
'comments': 5,
'isLiked': true,
},
];
final GlobalKey<PostsListState> _postsListKey = GlobalKey<PostsListState>();
@override
Widget build(BuildContext context) {
@ -77,236 +25,40 @@ class _FeedPageState extends State<FeedPage> {
child: Container(height: 1, color: Colors.grey[200]),
),
automaticallyImplyLeading: false,
actions: [
IconButton(
onPressed: () => _postsListKey.currentState?.refreshPosts(),
icon: Icon(Icons.refresh),
tooltip: 'Refresh',
),
body: RefreshIndicator(
onRefresh: () async {
await Future.delayed(Duration(seconds: 1));
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Feed refreshed!')));
},
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8),
itemCount: mockPosts.length,
itemBuilder: (context, index) {
return PostCard(
post: mockPosts[index],
onLikePressed: () {
setState(() {
if (mockPosts[index]['isLiked']) {
mockPosts[index]['likes']--;
mockPosts[index]['isLiked'] = false;
} else {
mockPosts[index]['likes']++;
mockPosts[index]['isLiked'] = true;
}
});
},
);
},
],
),
body: PostsList(
key: _postsListKey,
fetchPosts: PostService.getAllPosts,
emptyStateTitle: 'Nothing here..',
emptyStateSubtitle: 'Create the first post!',
showRefreshIndicator: true,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Create post functionality coming soon!')),
);
},
onPressed: _navigateToCreatePost,
backgroundColor: Color(0xFF6A4C93),
child: Icon(Icons.edit, color: Colors.white),
),
);
}
}
class PostCard extends StatefulWidget {
final Map<String, dynamic> post;
final VoidCallback onLikePressed;
const PostCard({Key? key, required this.post, required this.onLikePressed})
: super(key: key);
@override
_PostCardState createState() => _PostCardState();
}
class _PostCardState extends State<PostCard>
with SingleTickerProviderStateMixin {
late AnimationController _likeAnimationController;
late Animation<double> _likeAnimation;
@override
void initState() {
super.initState();
_likeAnimationController = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
_likeAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
CurvedAnimation(
parent: _likeAnimationController,
curve: Curves.elasticOut,
),
);
}
@override
void dispose() {
_likeAnimationController.dispose();
super.dispose();
}
void _handleLike() {
widget.onLikePressed();
_likeAnimationController.forward().then((_) {
_likeAnimationController.reverse();
});
}
@override
Widget build(BuildContext context) {
final user = widget.post['user'];
final isLiked = widget.post['isLiked'];
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: user['avatar_color'],
child: Text(
user['avatar'],
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user['displayName'],
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.black87,
),
),
SizedBox(height: 2),
Text(
widget.post['timestamp'],
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
IconButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('More options pressed')),
);
},
icon: Icon(Icons.more_horiz, color: Colors.grey[600]),
),
],
),
SizedBox(height: 12),
Text(
widget.post['content'],
style: TextStyle(
fontSize: 15,
height: 1.4,
color: Colors.black87,
),
),
SizedBox(height: 16),
Row(
children: [
AnimatedBuilder(
animation: _likeAnimation,
builder: (context, child) {
return Transform.scale(
scale: _likeAnimation.value,
child: GestureDetector(
onTap: _handleLike,
child: Container(
padding: EdgeInsets.all(12),
child: Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : Colors.grey[600],
size: 24,
),
),
),
);
},
),
Text(
'${widget.post['likes']}',
style: TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 16),
IconButton(
onPressed: () {
ScaffoldMessenger.of(
Future<void> _navigateToCreatePost() async {
final result = await Navigator.push(
context,
).showSnackBar(SnackBar(content: Text('Comments pressed')));
},
icon: Icon(
Icons.chat_bubble_outline,
color: Colors.grey[600],
size: 24,
),
),
Text(
'${widget.post['comments']}',
style: TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
Spacer(),
IconButton(
onPressed: () {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Share pressed')));
},
icon: Icon(
Icons.share_outlined,
color: Colors.grey[600],
size: 24,
),
),
],
),
],
),
),
MaterialPageRoute(builder: (context) => CreatePostScreen()),
);
// If post was created successfully, refresh the feed
if (result == true) {
_postsListKey.currentState?.refreshPosts();
}
}
}

View File

@ -3,6 +3,10 @@ import 'package:flutter/services.dart';
import '../../services/notification_service.dart';
import '../../services/user_service.dart';
import '../../services/auth_service.dart';
import '../../services/post_service.dart';
import '../../models/post_models.dart';
import '../../widgets/posts_list.dart';
import '../../utils/password_generator.dart';
class ProfilePage extends StatefulWidget {
@override
@ -15,42 +19,15 @@ class _ProfilePageState extends State<ProfilePage> {
bool isLoading = false;
Map<String, dynamic>? userData;
bool isLoadingUser = true;
final List<Map<String, dynamic>> mockUserPosts = [
{
'id': '1',
'content':
'Just finished working on a new Flutter project! The development experience keeps getting better.',
'timestamp': '2 hours ago',
'likes': 15,
'comments': 4,
'isLiked': true,
},
{
'id': '2',
'content':
'Beautiful sunset from my office window today. Sometimes you need to take a moment to appreciate the simple things.',
'timestamp': '1 day ago',
'likes': 23,
'comments': 8,
'isLiked': false,
},
{
'id': '3',
'content':
'Excited to share that I completed my certification today! Hard work pays off.',
'timestamp': '3 days ago',
'likes': 42,
'comments': 12,
'isLiked': true,
},
];
int totalLikes = 0;
int totalPosts = 0;
@override
void initState() {
super.initState();
_loadFCMToken();
_loadUserData();
_loadUserStats();
}
@override
@ -98,6 +75,21 @@ class _ProfilePageState extends State<ProfilePage> {
});
}
Future<void> _loadUserStats() async {
final result = await PostService.getUserPosts();
setState(() {
if (result['success'] == true) {
final posts = result['posts'] as List<Post>;
totalPosts = posts.length;
totalLikes = posts.fold(0, (sum, post) => sum + post.likes);
} else {
totalPosts = 0;
totalLikes = 0;
}
});
}
void _showErrorAlert(String message) {
showDialog(
context: context,
@ -223,7 +215,7 @@ class _ProfilePageState extends State<ProfilePage> {
Column(
children: [
Text(
'${mockUserPosts.length}',
'$totalPosts',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@ -242,7 +234,7 @@ class _ProfilePageState extends State<ProfilePage> {
Column(
children: [
Text(
'127',
'$totalLikes',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@ -250,7 +242,7 @@ class _ProfilePageState extends State<ProfilePage> {
),
),
Text(
'Followers',
'Likes Received',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
@ -270,126 +262,16 @@ class _ProfilePageState extends State<ProfilePage> {
margin: EdgeInsets.symmetric(horizontal: 16),
),
SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16),
itemCount: mockUserPosts.length,
itemBuilder: (context, index) {
final post = mockUserPosts[index];
return Container(
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: Color(0xFF6A4C93),
child: Text(
(userData!['displayName'] ?? 'U')
.substring(0, 1)
.toUpperCase(),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userData!['displayName'] ??
'Unknown User',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: Colors.black87,
),
),
Text(
post['timestamp'],
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
],
),
SizedBox(height: 12),
Text(
post['content'],
style: TextStyle(
fontSize: 15,
height: 1.4,
color: Colors.black87,
),
),
SizedBox(height: 12),
Row(
children: [
Icon(
post['isLiked']
? Icons.favorite
: Icons.favorite_border,
color: post['isLiked']
? Colors.red
: Colors.grey[600],
size: 20,
),
SizedBox(width: 4),
Text(
'${post['likes']}',
style: TextStyle(
color: Colors.grey[700],
fontSize: 14,
),
),
SizedBox(width: 16),
Icon(
Icons.chat_bubble_outline,
color: Colors.grey[600],
size: 20,
),
SizedBox(width: 4),
Text(
'${post['comments']}',
style: TextStyle(
color: Colors.grey[700],
fontSize: 14,
),
),
],
),
],
),
),
);
ProfilePostsList(
fetchPosts: PostService.getUserPosts,
onStatsUpdate: (posts, likes) {
setState(() {
totalPosts = posts;
totalLikes = likes;
});
},
),
],
SizedBox(height: 24),
],
),
),
@ -407,15 +289,28 @@ class _SettingsPageState extends State<SettingsPage> {
final TextEditingController _tokenController = TextEditingController();
bool isLoading = false;
// Admin user creation
Map<String, dynamic>? userData;
bool isLoadingUser = true;
bool isCreatingUser = false;
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _displayNameController = TextEditingController();
@override
void initState() {
super.initState();
_loadFCMToken();
_loadUserData();
_generatePassword();
}
@override
void dispose() {
_tokenController.dispose();
_emailController.dispose();
_passwordController.dispose();
_displayNameController.dispose();
super.dispose();
}
@ -452,6 +347,68 @@ class _SettingsPageState extends State<SettingsPage> {
}
}
Future<void> _loadUserData() async {
setState(() {
isLoadingUser = true;
});
final result = await UserService.getCurrentUser();
setState(() {
isLoadingUser = false;
if (result['success'] == true) {
userData = result['data'];
} else {
userData = null;
}
});
}
void _generatePassword() {
_passwordController.text = PasswordGenerator.generateReadablePassword();
}
bool get _isAdmin {
return userData?['role'] == 'ADMIN';
}
Future<Map<String, dynamic>> _createUserAccount() async {
if (_emailController.text.trim().isEmpty ||
_displayNameController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please fill in all fields'),
backgroundColor: Colors.red,
),
);
return {'success': false, 'message': 'Please fill in all fields'};
}
setState(() {
isCreatingUser = true;
});
try {
final result = await UserService.createUser(
email: _emailController.text.trim(),
password: _passwordController.text,
displayName: _displayNameController.text.trim(),
);
setState(() {
isCreatingUser = false;
});
return result;
} catch (e) {
setState(() {
isCreatingUser = false;
});
return {'success': false, 'message': 'Error creating user: $e'};
}
}
void _signOut() async {
showDialog(
context: context,
@ -482,6 +439,193 @@ class _SettingsPageState extends State<SettingsPage> {
);
}
void _showCredentialsDialog(String email, String password) {
final credentialsText = 'Email: $email\nPassword: $password';
final TextEditingController credentialsController = TextEditingController(
text: credentialsText,
);
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text('Account Created Successfully!'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Share these credentials with the user:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 16),
TextField(
controller: credentialsController,
decoration: InputDecoration(
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(Icons.copy),
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: credentialsText),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Credentials copied to clipboard'),
backgroundColor: Color(0xFF6A4C93),
),
);
},
tooltip: 'Copy credentials',
),
),
maxLines: 2,
readOnly: true,
style: TextStyle(fontFamily: 'monospace', fontSize: 14),
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: Text('Done'),
),
],
);
},
);
}
void _showCreateAccountDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Text('Create Account'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
TextField(
controller: _displayNameController,
decoration: InputDecoration(
labelText: 'Display Name',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
setDialogState(() {
_generatePassword();
});
},
tooltip: 'Generate new password',
),
),
readOnly: true,
style: TextStyle(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Password is auto-generated for security',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: isCreatingUser
? null
: () async {
final email = _emailController.text.trim();
final password = _passwordController.text;
final result = await _createUserAccount();
if (mounted) {
Navigator.of(dialogContext).pop();
if (result['success']) {
_showCredentialsDialog(email, password);
// Clear form
_emailController.clear();
_displayNameController.clear();
_generatePassword();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
result['message'] ??
'Failed to create user',
),
backgroundColor: Colors.red,
),
);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: isCreatingUser
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text('Create'),
),
],
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -501,6 +645,37 @@ class _SettingsPageState extends State<SettingsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isLoadingUser && _isAdmin) ...[
Text(
'Admin Tools',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF6A4C93),
),
),
SizedBox(height: 16),
Container(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _showCreateAccountDialog(context),
icon: Icon(Icons.person_add, size: 18),
label: Text('Create an Account'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
),
),
),
SizedBox(height: 32),
],
Text(
'Development Tools',
style: TextStyle(

View File

@ -0,0 +1,158 @@
import 'dart:convert';
import '../models/post_models.dart';
import 'http_service.dart';
class PostService {
static Future<Map<String, dynamic>> getAllPosts() async {
try {
final response = await HttpService.get('/posts/all');
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? '',
'posts':
(responseData['data'] as List?)
?.map((post) => Post.fromJson(post))
.toList() ??
[],
};
} else {
return {
'success': false,
'message': responseData['message'] ?? 'Failed to fetch posts',
'posts': <Post>[],
};
}
} catch (e) {
print('Error fetching posts: $e');
return {
'success': false,
'message': 'Network error: $e',
'posts': <Post>[],
};
}
}
static Future<Map<String, dynamic>> getUserPosts() async {
try {
final response = await HttpService.get('/posts/user');
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? '',
'posts': (responseData['data'] as List?)?.map((post) => Post.fromJson(post)).toList() ?? [],
};
} else {
return {
'success': false,
'message': responseData['message'] ?? 'Failed to fetch user posts',
'posts': <Post>[],
};
}
} catch (e) {
print('Error fetching user posts: $e');
return {
'success': false,
'message': 'Network error: $e',
'posts': <Post>[],
};
}
}
static Future<Map<String, dynamic>> createPost(String body) async {
try {
final createPostRequest = CreatePostRequest(body: body);
final response = await HttpService.post(
'/posts/create',
createPostRequest.toJson(),
);
final responseData = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? '',
'post': responseData['data'] != null
? Post.fromJson(responseData['data'])
: null,
};
} else {
return {
'success': false,
'message': responseData['message'] ?? 'Failed to create post',
'post': null,
};
}
} catch (e) {
print('Error creating post: $e');
return {'success': false, 'message': 'Network error: $e', 'post': null};
}
}
static Future<Map<String, dynamic>> likePost(String postId) async {
try {
final response = await HttpService.post(
'/posts/like',
{'postId': postId},
);
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? '',
'post': responseData['data'] != null
? Post.fromJson(responseData['data'])
: null,
};
} else {
return {
'success': false,
'message': responseData['message'] ?? 'Failed to like post',
'post': null,
};
}
} catch (e) {
print('Error liking post: $e');
return {'success': false, 'message': 'Network error: $e', 'post': null};
}
}
static Future<Map<String, dynamic>> unlikePost(String postId) async {
try {
final response = await HttpService.post(
'/posts/unlike',
{'postId': postId},
);
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? '',
'post': responseData['data'] != null
? Post.fromJson(responseData['data'])
: null,
};
} else {
return {
'success': false,
'message': responseData['message'] ?? 'Failed to unlike post',
'post': null,
};
}
} catch (e) {
print('Error unliking post: $e');
return {'success': false, 'message': 'Network error: $e', 'post': null};
}
}
}

View File

@ -78,4 +78,39 @@ class UserService {
};
}
}
static Future<Map<String, dynamic>> createUser({
required String email,
required String password,
required String displayName,
}) async {
try {
final response = await HttpService.post('/admin/createUser', {
'email': email,
'password': password,
'displayName': displayName,
});
final responseData = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? 'User created successfully',
'data': responseData['data'],
};
} else {
return {
'success': false,
'message': responseData['message'] ?? 'Failed to create user',
};
}
} catch (e) {
print('Error creating user: $e');
return {
'success': false,
'message': 'Network error. Please check your connection.',
};
}
}
}

View File

@ -0,0 +1,16 @@
import 'dart:math';
class PasswordGenerator {
static const String _readableChars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
static final Random _random = Random();
static String generateReadablePassword({int length = 6}) {
StringBuffer password = StringBuffer();
for (int i = 0; i < length; i++) {
password.write(_readableChars[_random.nextInt(_readableChars.length)]);
}
return password.toString();
}
}

View File

@ -0,0 +1,577 @@
import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart';
import '../models/post_models.dart';
import '../services/post_service.dart';
import '../utils/invitation_utils.dart';
class PostsList extends StatefulWidget {
final Future<Map<String, dynamic>> Function() fetchPosts;
final String emptyStateTitle;
final String emptyStateSubtitle;
final bool showRefreshIndicator;
final Widget? floatingActionButton;
const PostsList({
Key? key,
required this.fetchPosts,
this.emptyStateTitle = 'Nothing here..',
this.emptyStateSubtitle = 'Create the first post!',
this.showRefreshIndicator = true,
this.floatingActionButton,
}) : super(key: key);
@override
PostsListState createState() => PostsListState();
}
class PostsListState extends State<PostsList> {
bool _isLoading = true;
List<Post> _posts = [];
String _errorMessage = '';
@override
void initState() {
super.initState();
_loadPosts();
}
Future<void> _loadPosts() async {
setState(() {
_isLoading = true;
_errorMessage = '';
});
try {
final result = await widget.fetchPosts();
setState(() {
if (result['success']) {
_posts = result['posts'];
} else {
_errorMessage = result['message'] ?? 'Failed to load posts';
}
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Network error: $e';
_isLoading = false;
});
}
}
Future<void> _refreshPosts() async {
await _loadPosts();
if (widget.showRefreshIndicator) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Posts refreshed!'),
backgroundColor: Color(0xFF6A4C93),
),
);
}
}
@override
Widget build(BuildContext context) {
Widget body = _buildBody();
if (widget.showRefreshIndicator) {
body = RefreshIndicator(
onRefresh: _refreshPosts,
child: body,
);
}
return body;
}
Widget _buildBody() {
if (_isLoading) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
),
);
}
if (_errorMessage.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
_errorMessage,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadPosts,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: Text('Try Again'),
),
],
),
);
}
if (_posts.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.post_add,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
widget.emptyStateTitle,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
),
),
SizedBox(height: 8),
Text(
widget.emptyStateSubtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
);
}
return ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8),
itemCount: _posts.length,
itemBuilder: (context, index) {
return PostCard(
post: _posts[index],
);
},
);
}
void refreshPosts() {
_loadPosts();
}
}
class PostCard extends StatefulWidget {
final Post post;
const PostCard({Key? key, required this.post})
: super(key: key);
@override
_PostCardState createState() => _PostCardState();
}
class _PostCardState extends State<PostCard> {
late Post _currentPost;
bool _isLiking = false;
@override
void initState() {
super.initState();
_currentPost = widget.post;
}
@override
void didUpdateWidget(PostCard oldWidget) {
super.didUpdateWidget(oldWidget);
// Update current post if the widget's post changed
if (oldWidget.post.id != widget.post.id) {
_currentPost = widget.post;
}
}
Color _getAvatarColor(String displayName) {
final colors = [
Color(0xFF32B0A5),
Color(0xFF4600B9),
Color(0xFF6A4C93),
Color(0xFFFF6347),
Color(0xFF32CD32),
Color(0xFF9932CC),
];
int hash = displayName.hashCode;
return colors[hash.abs() % colors.length];
}
String _getAvatarLetter(String displayName) {
return displayName.isNotEmpty ? displayName[0].toUpperCase() : '?';
}
void _sharePost(Post post) {
final shareText = '${post.creator.displayName} posted on Wesal.online:\n\n${post.body}';
Share.share(shareText);
}
Future<void> _toggleLike() async {
if (_isLiking) return; // Prevent multiple simultaneous requests
setState(() {
_isLiking = true;
// Optimistic update - immediately change UI
_currentPost = _currentPost.copyWith(
liked: !_currentPost.liked,
likes: _currentPost.liked ? _currentPost.likes - 1 : _currentPost.likes + 1,
);
});
try {
Map<String, dynamic> result;
if (widget.post.liked) {
result = await PostService.unlikePost(widget.post.id);
} else {
result = await PostService.likePost(widget.post.id);
}
if (result['success'] && result['post'] != null) {
// Update with server response
setState(() {
_currentPost = result['post'];
_isLiking = false;
});
} else {
// Revert optimistic update on failure
setState(() {
_currentPost = widget.post;
_isLiking = false;
});
// Show error message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result['message'] ?? 'Failed to update like'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
// Revert optimistic update on error
setState(() {
_currentPost = widget.post;
_isLiking = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Network error occurred'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final creator = _currentPost.creator;
final avatarColor = _getAvatarColor(creator.displayName);
final avatarLetter = _getAvatarLetter(creator.displayName);
final relativeTime = InvitationUtils.getRelativeTime(_currentPost.creationDate);
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: avatarColor,
child: Text(
avatarLetter,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
creator.displayName,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.black87,
),
),
SizedBox(height: 2),
Text(
relativeTime,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
IconButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('More options pressed')),
);
},
icon: Icon(Icons.more_horiz, color: Colors.grey[600]),
),
],
),
SizedBox(height: 12),
Text(
_currentPost.body,
style: TextStyle(
fontSize: 15,
height: 1.4,
color: Colors.black87,
),
),
SizedBox(height: 16),
Row(
children: [
IconButton(
onPressed: _toggleLike,
icon: Icon(
_currentPost.liked ? Icons.favorite : Icons.favorite_border,
color: _currentPost.liked ? Colors.red : Colors.grey[600],
size: 24,
),
),
Text(
'${_currentPost.likes}',
style: TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 16),
IconButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Comments feature coming soon!')),
);
},
icon: Icon(
Icons.chat_bubble_outline,
color: Colors.grey[600],
size: 24,
),
),
Text(
'${_currentPost.comments}',
style: TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
Spacer(),
IconButton(
onPressed: () => _sharePost(_currentPost),
icon: Icon(
Icons.share_outlined,
color: Colors.grey[600],
size: 24,
),
),
],
),
],
),
),
);
}
}
class ProfilePostsList extends StatefulWidget {
final Future<Map<String, dynamic>> Function() fetchPosts;
final Function(int posts, int likes)? onStatsUpdate;
const ProfilePostsList({
Key? key,
required this.fetchPosts,
this.onStatsUpdate,
}) : super(key: key);
@override
_ProfilePostsListState createState() => _ProfilePostsListState();
}
class _ProfilePostsListState extends State<ProfilePostsList> {
bool _isLoading = true;
List<Post> _posts = [];
String _errorMessage = '';
@override
void initState() {
super.initState();
_loadPosts();
}
Future<void> _loadPosts() async {
setState(() {
_isLoading = true;
_errorMessage = '';
});
try {
final result = await widget.fetchPosts();
setState(() {
if (result['success']) {
_posts = result['posts'];
// Update parent stats
if (widget.onStatsUpdate != null) {
final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes);
widget.onStatsUpdate!(_posts.length, totalLikes);
}
} else {
_errorMessage = result['message'] ?? 'Failed to load posts';
}
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Network error: $e';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Container(
padding: EdgeInsets.all(32),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF6A4C93),
),
),
),
);
}
if (_errorMessage.isNotEmpty) {
return Container(
padding: EdgeInsets.all(32),
child: Center(
child: Column(
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
_errorMessage,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadPosts,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: Text('Try Again'),
),
],
),
),
);
}
if (_posts.isEmpty) {
return Container(
padding: EdgeInsets.all(32),
child: Center(
child: Column(
children: [
Icon(
Icons.post_add,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
'No posts yet',
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Text(
'Start sharing your thoughts!',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
),
);
}
return Column(
children: [
..._posts.map((post) =>
Container(
margin: EdgeInsets.only(bottom: 16, left: 16, right: 16),
child: PostCard(post: post),
)
),
SizedBox(height: 24),
],
);
}
}

View File

@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -9,10 +9,12 @@ import firebase_core
import firebase_messaging
import flutter_secure_storage_macos
import path_provider_foundation
import share_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
}

View File

@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
@ -89,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
firebase_core:
dependency: "direct main"
description:
@ -137,6 +153,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.10.9"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@ -304,6 +328,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
@ -376,6 +408,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
url: "https://pub.dev"
source: hosted
version: "11.0.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
sky_engine:
dependency: transitive
description: flutter
@ -389,6 +437,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@ -437,6 +493,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math:
dependency: transitive
description:

View File

@ -15,6 +15,7 @@ dependencies:
http: ^1.1.0
googleapis_auth: ^1.6.0
flutter_secure_storage: ^9.2.2
share_plus: ^11.0.0
dev_dependencies:
flutter_test:

View File

@ -8,10 +8,16 @@
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -5,6 +5,8 @@
list(APPEND FLUTTER_PLUGIN_LIST
firebase_core
flutter_secure_storage_windows
share_plus
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST