first commit
This commit is contained in:
112
lib/features/admin/data/services/admin_service.dart
Normal file
112
lib/features/admin/data/services/admin_service.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../../../music/domain/models/track_model.dart';
|
||||
import '../../domain/models/admin_panel_data_model.dart';
|
||||
import '../../domain/models/admin_post_model.dart';
|
||||
import '../../domain/models/admin_user_model.dart';
|
||||
|
||||
class AdminService {
|
||||
const AdminService(this._client);
|
||||
|
||||
final SupabaseClient _client;
|
||||
|
||||
Future<AdminPanelDataModel> fetchPanelData() async {
|
||||
// Fetch all profiles
|
||||
final profileRows = List<Map<String, dynamic>>.from(
|
||||
await _client.from('profiles').select('user_id, username, avatar_url, banned, featured'),
|
||||
);
|
||||
|
||||
// Fetch latest posts
|
||||
final postRows = List<Map<String, dynamic>>.from(
|
||||
await _client
|
||||
.from('posts')
|
||||
.select('id, user_id, caption, image_url, likes_count, featured')
|
||||
.order('created_at', ascending: false)
|
||||
.limit(100),
|
||||
);
|
||||
|
||||
// Fetch tracks
|
||||
final trackRows = List<Map<String, dynamic>>.from(
|
||||
await _client
|
||||
.from('tracks')
|
||||
.select('*, profiles(username)')
|
||||
.order('created_at', ascending: false)
|
||||
.limit(100),
|
||||
);
|
||||
|
||||
final userMap = <String, AdminUserModel>{};
|
||||
for (final row in profileRows) {
|
||||
final user = AdminUserModel(
|
||||
userId: row['user_id'] as String,
|
||||
username: (row['username'] as String?) ?? 'RIOTER',
|
||||
avatarUrl: (row['avatar_url'] as String?) ?? '',
|
||||
banned: (row['banned'] as bool?) ?? false,
|
||||
featured: (row['featured'] as bool?) ?? false,
|
||||
);
|
||||
userMap[user.userId] = user;
|
||||
}
|
||||
|
||||
final users = userMap.values.toList()
|
||||
..sort((a, b) => a.username.toLowerCase().compareTo(b.username.toLowerCase()));
|
||||
|
||||
final posts = postRows.map((row) {
|
||||
final user = userMap[row['user_id']] ??
|
||||
AdminUserModel(
|
||||
userId: row['user_id'] as String,
|
||||
username: 'RIOTER',
|
||||
avatarUrl: '',
|
||||
banned: false,
|
||||
featured: false,
|
||||
);
|
||||
return AdminPostModel(
|
||||
id: row['id'] as String,
|
||||
userId: row['user_id'] as String,
|
||||
username: user.username,
|
||||
imageUrl: (row['image_url'] as String?) ?? '',
|
||||
caption: (row['caption'] as String?) ?? '',
|
||||
likesCount: (row['likes_count'] as int?) ?? 0,
|
||||
featured: (row['featured'] as bool?) ?? false,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final tracks = trackRows.map((row) {
|
||||
final username = (row['profiles'] as Map<String, dynamic>?)?['username'] as String?;
|
||||
return TrackModel.fromJson(row, username: username);
|
||||
}).toList();
|
||||
|
||||
return AdminPanelDataModel(users: users, posts: posts, tracks: tracks);
|
||||
}
|
||||
|
||||
Future<void> deletePost(String postId) async {
|
||||
await _client.from('posts').delete().eq('id', postId);
|
||||
}
|
||||
|
||||
Future<void> setUserBanned({
|
||||
required String userId,
|
||||
required bool banned,
|
||||
}) async {
|
||||
await _client.from('profiles').update({'banned': banned}).eq('user_id', userId);
|
||||
}
|
||||
|
||||
Future<void> setUserFeatured({
|
||||
required String userId,
|
||||
required bool featured,
|
||||
}) async {
|
||||
await _client.from('profiles').update({'featured': featured}).eq('user_id', userId);
|
||||
}
|
||||
|
||||
Future<void> setPostFeatured({
|
||||
required String postId,
|
||||
required bool featured,
|
||||
}) async {
|
||||
await _client.from('posts').update({'featured': featured}).eq('id', postId);
|
||||
}
|
||||
|
||||
Future<void> setTrackFeatured({
|
||||
required String trackId,
|
||||
required bool featured,
|
||||
}) async {
|
||||
// Assuming tracks table has a 'featured' column
|
||||
await _client.from('tracks').update({'featured': featured}).eq('id', trackId);
|
||||
}
|
||||
}
|
||||
15
lib/features/admin/domain/models/admin_panel_data_model.dart
Normal file
15
lib/features/admin/domain/models/admin_panel_data_model.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import '../../../music/domain/models/track_model.dart';
|
||||
import 'admin_post_model.dart';
|
||||
import 'admin_user_model.dart';
|
||||
|
||||
class AdminPanelDataModel {
|
||||
const AdminPanelDataModel({
|
||||
required this.users,
|
||||
required this.posts,
|
||||
required this.tracks,
|
||||
});
|
||||
|
||||
final List<AdminUserModel> users;
|
||||
final List<AdminPostModel> posts;
|
||||
final List<TrackModel> tracks;
|
||||
}
|
||||
19
lib/features/admin/domain/models/admin_post_model.dart
Normal file
19
lib/features/admin/domain/models/admin_post_model.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
class AdminPostModel {
|
||||
const AdminPostModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.imageUrl,
|
||||
required this.caption,
|
||||
required this.likesCount,
|
||||
required this.featured,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String username;
|
||||
final String imageUrl;
|
||||
final String caption;
|
||||
final int likesCount;
|
||||
final bool featured;
|
||||
}
|
||||
15
lib/features/admin/domain/models/admin_user_model.dart
Normal file
15
lib/features/admin/domain/models/admin_user_model.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class AdminUserModel {
|
||||
const AdminUserModel({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.avatarUrl,
|
||||
required this.banned,
|
||||
required this.featured,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final String username;
|
||||
final String avatarUrl;
|
||||
final bool banned;
|
||||
final bool featured;
|
||||
}
|
||||
189
lib/features/admin/presentation/pages/admin_page.dart
Normal file
189
lib/features/admin/presentation/pages/admin_page.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../shared/widgets/riotz_shell.dart';
|
||||
import '../../domain/models/admin_post_model.dart';
|
||||
import '../../domain/models/admin_user_model.dart';
|
||||
import '../providers/admin_providers.dart';
|
||||
|
||||
class AdminPage extends ConsumerWidget {
|
||||
const AdminPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isAdmin = ref.watch(isAdminProvider);
|
||||
final dataAsync = ref.watch(adminPanelDataProvider);
|
||||
|
||||
ref.listen(adminControllerProvider, (_, next) {
|
||||
next.whenOrNull(
|
||||
data: (_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Admin action applied')),
|
||||
);
|
||||
},
|
||||
error: (error, _) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error.toString())),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!isAdmin) {
|
||||
return const RiotzShell(
|
||||
title: 'RIOTZ // Admin',
|
||||
child: Center(
|
||||
child: Text('You are not authorized for admin panel access.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RiotzShell(
|
||||
title: 'RIOTZ // Admin',
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async => ref.invalidate(adminPanelDataProvider),
|
||||
child: dataAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('Error: $error')),
|
||||
data: (data) => ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Text(
|
||||
'Whitelist-based Admin Panel',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Users',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...data.users.map(
|
||||
(user) => _UserModerationCard(
|
||||
user: user,
|
||||
onToggleBan: (banned) => ref
|
||||
.read(adminControllerProvider.notifier)
|
||||
.setUserBanned(userId: user.userId, banned: banned),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
'Posts',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...data.posts.map(
|
||||
(post) => _PostModerationCard(
|
||||
post: post,
|
||||
onDelete: () =>
|
||||
ref.read(adminControllerProvider.notifier).deletePost(post.id),
|
||||
onToggleFeatured: (featured) => ref
|
||||
.read(adminControllerProvider.notifier)
|
||||
.setPostFeatured(postId: post.id, featured: featured),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserModerationCard extends StatelessWidget {
|
||||
const _UserModerationCard({
|
||||
required this.user,
|
||||
required this.onToggleBan,
|
||||
});
|
||||
|
||||
final AdminUserModel user;
|
||||
final ValueChanged<bool> onToggleBan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: SwitchListTile(
|
||||
value: user.banned,
|
||||
onChanged: onToggleBan,
|
||||
title: Text(user.username),
|
||||
subtitle: Text(user.userId),
|
||||
secondary: CircleAvatar(
|
||||
backgroundImage:
|
||||
user.avatarUrl.isNotEmpty ? NetworkImage(user.avatarUrl) : null,
|
||||
child: user.avatarUrl.isEmpty ? const Icon(Icons.person) : null,
|
||||
),
|
||||
activeThumbColor: Colors.redAccent,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostModerationCard extends StatelessWidget {
|
||||
const _PostModerationCard({
|
||||
required this.post,
|
||||
required this.onDelete,
|
||||
required this.onToggleFeatured,
|
||||
});
|
||||
|
||||
final AdminPostModel post;
|
||||
final VoidCallback onDelete;
|
||||
final ValueChanged<bool> onToggleFeatured;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
post.username,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onDelete,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (post.caption.isNotEmpty) ...[
|
||||
Text(post.caption),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (post.imageUrl.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
post.imageUrl,
|
||||
width: double.infinity,
|
||||
height: 160,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text('${post.likesCount} likes'),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Feature'),
|
||||
Switch(
|
||||
value: post.featured,
|
||||
onChanged: onToggleFeatured,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/config/admin_whitelist.dart';
|
||||
import '../../../../core/supabase/supabase_providers.dart';
|
||||
import '../../../auth/presentation/providers/auth_provider.dart';
|
||||
import '../../data/services/admin_service.dart';
|
||||
import '../../domain/models/admin_panel_data_model.dart';
|
||||
|
||||
final adminServiceProvider = Provider<AdminService>((ref) {
|
||||
final client = ref.watch(supabaseProvider);
|
||||
return AdminService(client);
|
||||
});
|
||||
|
||||
final isAdminProvider = Provider<bool>((ref) {
|
||||
final user = ref.watch(authStateProvider).value?.session?.user;
|
||||
return AdminWhitelist.isAdmin(user?.email);
|
||||
});
|
||||
|
||||
final adminPanelDataProvider = FutureProvider<AdminPanelDataModel>((ref) async {
|
||||
final isAdmin = ref.watch(isAdminProvider);
|
||||
if (!isAdmin) {
|
||||
throw StateError('UNAUTHORIZED ACCESS DETECTED.');
|
||||
}
|
||||
return ref.watch(adminServiceProvider).fetchPanelData();
|
||||
});
|
||||
|
||||
final adminControllerProvider =
|
||||
AutoDisposeAsyncNotifierProvider<AdminController, void>(
|
||||
AdminController.new,
|
||||
);
|
||||
|
||||
class AdminController extends AutoDisposeAsyncNotifier<void> {
|
||||
@override
|
||||
Future<void> build() async {}
|
||||
|
||||
Future<void> deletePost(String postId) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(adminServiceProvider).deletePost(postId);
|
||||
ref.invalidate(adminPanelDataProvider);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setUserBanned({
|
||||
required String userId,
|
||||
required bool banned,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(adminServiceProvider).setUserBanned(
|
||||
userId: userId,
|
||||
banned: banned,
|
||||
);
|
||||
ref.invalidate(adminPanelDataProvider);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setUserFeatured({
|
||||
required String userId,
|
||||
required bool featured,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(adminServiceProvider).setUserFeatured(
|
||||
userId: userId,
|
||||
featured: featured,
|
||||
);
|
||||
ref.invalidate(adminPanelDataProvider);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setPostFeatured({
|
||||
required String postId,
|
||||
required bool featured,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(adminServiceProvider).setPostFeatured(
|
||||
postId: postId,
|
||||
featured: featured,
|
||||
);
|
||||
ref.invalidate(adminPanelDataProvider);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setTrackFeatured({
|
||||
required String trackId,
|
||||
required bool featured,
|
||||
}) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
await ref.read(adminServiceProvider).setTrackFeatured(
|
||||
trackId: trackId,
|
||||
featured: featured,
|
||||
);
|
||||
ref.invalidate(adminPanelDataProvider);
|
||||
});
|
||||
}
|
||||
}
|
||||
244
lib/features/admin/presentation/screens/admin_screen.dart
Normal file
244
lib/features/admin/presentation/screens/admin_screen.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/widgets/riotz_scaffold.dart';
|
||||
import '../../../../core/widgets/riotz_button.dart';
|
||||
import '../../../music/domain/models/track_model.dart';
|
||||
import '../providers/admin_providers.dart';
|
||||
import '../../domain/models/admin_user_model.dart';
|
||||
import '../../domain/models/admin_post_model.dart';
|
||||
|
||||
class AdminScreen extends ConsumerWidget {
|
||||
const AdminScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final dataAsync = ref.watch(adminPanelDataProvider);
|
||||
|
||||
ref.listen(adminControllerProvider, (_, next) {
|
||||
next.whenOrNull(
|
||||
data: (_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
backgroundColor: AppColors.success,
|
||||
content: Text('SYSTEM OVERRIDE SUCCESSFUL.', style: TextStyle(color: AppColors.black, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, _) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: AppColors.bloodRed,
|
||||
content: Text('ERROR: ${error.toString().toUpperCase()}'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return RiotzScaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('RIOTZ // TERMINAL'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
),
|
||||
body: dataAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator(color: AppColors.neonRed)),
|
||||
error: (error, _) => Center(child: Text('TERMINAL OFFLINE: $error')),
|
||||
data: (data) => RefreshIndicator(
|
||||
onRefresh: () async => ref.invalidate(adminPanelDataProvider),
|
||||
color: AppColors.neonRed,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||
children: [
|
||||
_SectionHeader(title: 'USER MODERATION', count: data.users.length),
|
||||
const SizedBox(height: 16),
|
||||
...data.users.map((user) => _AdminUserCard(user: user)),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
_SectionHeader(title: 'TRACK MODERATION', count: data.tracks.length),
|
||||
const SizedBox(height: 16),
|
||||
...data.tracks.map((track) => _AdminTrackCard(track: track)),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
_SectionHeader(title: 'POST MODERATION', count: data.posts.length),
|
||||
const SizedBox(height: 16),
|
||||
...data.posts.map((post) => _AdminPostCard(post: post)),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({required this.title, required this.count});
|
||||
final String title;
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.labelLarge?.copyWith(letterSpacing: 2, color: AppColors.neonRed)),
|
||||
const Divider(color: AppColors.neonRed, thickness: 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AdminUserCard extends ConsumerWidget {
|
||||
const _AdminUserCard({required this.user});
|
||||
final AdminUserModel user;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border.all(color: user.banned ? AppColors.bloodRed : AppColors.border),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColors.surfaceLight,
|
||||
backgroundImage: user.avatarUrl.isNotEmpty ? NetworkImage(user.avatarUrl) : null,
|
||||
child: user.avatarUrl.isEmpty ? const Icon(Icons.person, color: AppColors.grey) : null,
|
||||
),
|
||||
title: Text(user.username.toUpperCase(), style: theme.textTheme.labelLarge),
|
||||
subtitle: Text(user.banned ? 'STATUS: BANNED' : (user.featured ? 'STATUS: FEATURED ARTIST' : 'STATUS: ACTIVE'),
|
||||
style: TextStyle(color: user.banned ? AppColors.neonRed : (user.featured ? AppColors.success : AppColors.grey), fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
trailing: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert, color: AppColors.white),
|
||||
color: AppColors.surfaceLight,
|
||||
onSelected: (value) {
|
||||
if (value == 'ban') {
|
||||
ref.read(adminControllerProvider.notifier).setUserBanned(userId: user.userId, banned: !user.banned);
|
||||
} else if (value == 'feature') {
|
||||
ref.read(adminControllerProvider.notifier).setUserFeatured(userId: user.userId, featured: !user.featured);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'ban',
|
||||
child: Text(user.banned ? 'UNBAN USER' : 'BAN USER', style: const TextStyle(color: AppColors.neonRed)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'feature',
|
||||
child: Text(user.featured ? 'UNFEATURE ARTIST' : 'FEATURE ARTIST'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AdminTrackCard extends ConsumerWidget {
|
||||
const _AdminTrackCard({required this.track});
|
||||
final TrackModel track;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border.all(color: track.featured ? AppColors.success : AppColors.border),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.music_note, color: track.featured ? AppColors.success : AppColors.white),
|
||||
title: Text(track.title.toUpperCase(), style: theme.textTheme.labelLarge),
|
||||
subtitle: Text('BY: ${track.username.toUpperCase()}', style: const TextStyle(fontSize: 10, color: AppColors.grey)),
|
||||
trailing: Switch(
|
||||
value: track.featured,
|
||||
activeColor: AppColors.success,
|
||||
onChanged: (val) {
|
||||
ref.read(adminControllerProvider.notifier).setTrackFeatured(trackId: track.id, featured: val);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AdminPostCard extends ConsumerWidget {
|
||||
const _AdminPostCard({required this.post});
|
||||
final AdminPostModel post;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border.all(color: post.featured ? AppColors.neonRed : AppColors.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(post.username.toUpperCase(), style: theme.textTheme.labelLarge),
|
||||
subtitle: Text('ID: ${post.id.substring(0, 8)}...', style: const TextStyle(fontSize: 10)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_forever, color: AppColors.neonRed),
|
||||
onPressed: () => _confirmDelete(context, ref),
|
||||
),
|
||||
),
|
||||
if (post.imageUrl.isNotEmpty)
|
||||
Image.network(post.imageUrl, height: 120, width: double.infinity, fit: BoxFit.cover),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('FEATURE CONTENT', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: post.featured,
|
||||
activeColor: AppColors.neonRed,
|
||||
onChanged: (val) {
|
||||
ref.read(adminControllerProvider.notifier).setPostFeatured(postId: post.id, featured: val);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||
title: const Text('ERASE DATA?'),
|
||||
content: const Text('THIS WILL REMOVE THE CONTENT FROM THE NETWORK PERMANENTLY.'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('CANCEL')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(adminControllerProvider.notifier).deletePost(post.id);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('DELETE', style: TextStyle(color: AppColors.neonRed)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user