feat: implement file sharing and management functionality

- Add config.dart for API configuration
- Implement sidebar navigation with logout functionality
- Add file sharing dialog with public/private options
- Create shares page to view and manage shared files
- Implement file upload/download/delete operations
- Add authentication persistence using shared preferences
- Configure CORS for API to enable cross-origin requests
This commit is contained in:
Mercurio 2025-06-02 15:37:06 +02:00
parent e6dc119357
commit a95d455b2e
9 changed files with 1015 additions and 35 deletions

View file

@ -1,10 +1,9 @@
#[macro_use]
extern crate rocket;
use rocket::{fs::FileServer};
use rocket::{fs::FileServer, Request};
use rocket::serde::json::Json;
use rocket::http::Status;
use rocket::Request;
use rocket::response::status::Custom;
use sqlx::postgres::PgPoolOptions;
@ -12,6 +11,9 @@ use sqlx::postgres::PgPoolOptions;
use std::env;
use dotenv::dotenv;
// CORS
use rocket_cors::{CorsOptions, AllowedOrigins};
mod auth;
mod encryption;
mod file;
@ -28,6 +30,11 @@ fn not_found(_: &Request) -> Custom<Json<&'static str>> {
Custom(Status::NotFound, Json("Not Found"))
}
#[options("/<_..>")]
fn all_options() -> &'static str {
""
}
#[rocket::main]
async fn main() -> Result<(), rocket::Error> {
dotenv().ok();
@ -43,7 +50,13 @@ async fn main() -> Result<(), rocket::Error> {
.merge(("port", 8082))
.merge(("address", "0.0.0.0"));
// Create permissive CORS configuration (adjust as needed)
let cors = CorsOptions::default()
.to_cors()
.expect("Failed to create CORS fairing");
rocket::custom(figment)
.attach(cors)
.manage(pool)
.mount("/api", routes![
register,
@ -58,9 +71,10 @@ async fn main() -> Result<(), rocket::Error> {
share_file,
update_role,
update_quota,
all_options, // handles OPTIONS preflight
])
.mount("/", FileServer::from("/app/static").rank(10))
.register("/", catchers![not_found])
//.mount("/", FileServer::from("/app/static").rank(10))
//.register("/", catchers![not_found])
.launch()
.await?;

View file

@ -7,6 +7,9 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
errors:
use_build_context_synchronously: ignore
include: package:flutter_lints/flutter.yaml
linter:

View file

@ -0,0 +1,9 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
class Config {
static String get apiBaseUrl =>
dotenv.env['API_URL'] ?? 'http://localhost:8082';
static String get apiPath => '/api';
static String get apiUrl => '$apiBaseUrl$apiPath';
}

View file

@ -1,6 +1,13 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'mainpage.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'config.dart';
void main() {
Future<void> main() async {
await dotenv.load();
runApp(const MyApp());
}
@ -9,14 +16,24 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Litecloud alpha',
theme: ThemeData(
colorScheme:
ColorScheme.fromSeed(seedColor: Color.fromARGB(255, 72, 4, 117)),
useMaterial3: true,
),
home: const AuthScreen(),
return FutureBuilder<SharedPreferences>(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
final prefs = snapshot.data!;
final jwt = prefs.getString('jwt');
return MaterialApp(
title: 'Litecloud alpha',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 72, 4, 117)),
useMaterial3: true,
),
home: jwt != null ? const MainPage() : const AuthScreen(),
);
},
);
}
}
@ -30,13 +47,78 @@ class AuthScreen extends StatefulWidget {
class _AuthScreenState extends State<AuthScreen> {
bool _isLogin = true;
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmController = TextEditingController();
bool _loading = false;
String? _error;
void _toggleAuthMode() {
setState(() {
_isLogin = !_isLogin;
_error = null;
});
}
Future<void> _submit() async {
setState(() {
_loading = true;
_error = null;
});
final email = _emailController.text.trim();
final password = _passwordController.text.trim();
if (!_isLogin && password != _confirmController.text.trim()) {
setState(() {
_error = "Le password non coincidono";
_loading = false;
});
return;
}
final url =
Uri.parse('${Config.apiUrl}/${_isLogin ? "login" : "register"}');
final body = jsonEncode({
"username": email,
"password": password,
});
try {
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: body,
);
if (response.statusCode == 200 || response.statusCode == 201) {
final data = jsonDecode(response.body);
final token = data['token'];
final prefs = await SharedPreferences.getInstance();
await prefs.setString('jwt', token);
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MainPage()),
);
}
} else {
setState(() {
_error = "Credenziali non valide";
});
}
} catch (e) {
setState(() {
_error = "Errore di rete";
});
} finally {
setState(() {
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
@ -45,7 +127,7 @@ class _AuthScreenState extends State<AuthScreen> {
body: Container(
width: deviceSize.width,
height: deviceSize.height,
decoration: BoxDecoration(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple, Color.fromARGB(255, 72, 4, 117)],
begin: Alignment.topLeft,
@ -110,15 +192,17 @@ class _AuthScreenState extends State<AuthScreen> {
),
const SizedBox(height: 40),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
labelText: 'Username',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
@ -129,6 +213,7 @@ class _AuthScreenState extends State<AuthScreen> {
if (!_isLogin) const SizedBox(height: 16),
if (!_isLogin)
TextFormField(
controller: _confirmController,
decoration: const InputDecoration(
labelText: 'Confirm Password',
prefixIcon: Icon(Icons.lock),
@ -138,15 +223,23 @@ class _AuthScreenState extends State<AuthScreen> {
),
const SizedBox(height: 24),
FilledButton(
onPressed: () {},
onPressed: _loading ? null : _submit,
style: FilledButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
),
child: Text(
_isLogin ? 'LOGIN' : 'REGISTER',
style: const TextStyle(fontSize: 16),
),
child: _loading
? const CircularProgressIndicator()
: Text(
_isLogin ? 'LOGIN' : 'REGISTER',
style: const TextStyle(fontSize: 16),
),
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(_error!,
style: const TextStyle(color: Colors.red)),
),
const SizedBox(height: 12),
TextButton(
onPressed: _toggleAuthMode,

View file

@ -0,0 +1,399 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:file_picker/file_picker.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'main.dart';
import 'sidebar.dart';
import 'shares.dart';
import 'sharedialog.dart';
import 'package:path_provider/path_provider.dart';
import 'package:file_saver/file_saver.dart';
import 'dart:io';
import 'config.dart';
class FileInfo {
final int id;
final String name;
final int size;
final DateTime uploadedAt;
FileInfo({
required this.id,
required this.name,
required this.size,
required this.uploadedAt,
});
factory FileInfo.fromJson(Map<String, dynamic> json) {
return FileInfo(
id: json['id'],
name: json['original_name'],
size: json['size'],
uploadedAt: DateTime.parse(json['uploaded_at']),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
SidebarPage _selectedPage = SidebarPage.myFiles;
List<FileInfo> _files = [];
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_fetchFiles();
}
Future<void> _fetchFiles() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final prefs = await SharedPreferences.getInstance();
final jwt = prefs.getString('jwt');
if (jwt == null) {
setState(() {
_error = 'Sessione scaduta, effettua di nuovo il login.';
_isLoading = false;
});
return;
}
final uri = Uri.parse('${Config.apiUrl}/files');
final response = await http.get(
uri,
headers: {'Authorization': 'Bearer $jwt'},
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
setState(() {
_files = data.map((file) => FileInfo.fromJson(file)).toList();
_isLoading = false;
});
} else {
setState(() {
_error = 'Errore durante il recupero dei file.';
_isLoading = false;
});
}
} catch (e) {
setState(() {
_error = 'Errore di rete: ${e.toString()}';
_isLoading = false;
});
}
}
Future<void> _logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('jwt');
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const AuthScreen()),
(route) => false,
);
}
}
void _onPageSelected(SidebarPage page) {
setState(() {
_selectedPage = page;
});
}
Future<void> _uploadFile() async {
final result = await FilePicker.platform.pickFiles();
if (result == null || result.files.isEmpty) return;
final file = result.files.first;
final prefs = await SharedPreferences.getInstance();
final jwt = prefs.getString('jwt');
if (jwt == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sessione scaduta, effettua di nuovo il login.')),
);
return;
}
final uri = Uri.parse('${Config.apiUrl}/upload');
final request = http.MultipartRequest('POST', uri)
..headers['Authorization'] = 'Bearer $jwt'
..files.add(
http.MultipartFile.fromBytes(
'file',
file.bytes!,
filename: file.name,
),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Caricamento in corso...')),
);
final response = await request.send();
if (response.statusCode == 200 || response.statusCode == 201) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('File caricato con successo!')),
);
_fetchFiles(); // Refresh the file list
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Errore durante l\'upload.')),
);
}
}
Future<void> _downloadFile(FileInfo file) async {
try {
final prefs = await SharedPreferences.getInstance();
final jwt = prefs.getString('jwt');
if (jwt == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sessione scaduta, effettua di nuovo il login.')),
);
return;
}
final uri = Uri.parse('${Config.apiUrl}/files/${file.id}');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Download in corso...')),
);
final response = await http.get(
uri,
headers: {'Authorization': 'Bearer $jwt'},
);
if (response.statusCode == 200) {
// Save the file to disk
if (Platform.isAndroid || Platform.isIOS) {
// Mobile platforms
await FileSaver.instance.saveFile(
name: file.name,
bytes: response.bodyBytes,
);
} else {
// Desktop platforms
final directory = await getDownloadsDirectory();
if (directory != null) {
final filePath = '${directory.path}/${file.name}';
final fileObj = File(filePath);
await fileObj.writeAsBytes(response.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('File salvato in: $filePath')),
);
} else {
throw Exception('Could not access downloads directory');
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('File ${file.name} scaricato con successo!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Errore durante il download.')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore di rete: ${e.toString()}')),
);
}
}
Future<void> _deleteFile(FileInfo file) async {
try {
final prefs = await SharedPreferences.getInstance();
final jwt = prefs.getString('jwt');
if (jwt == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sessione scaduta, effettua di nuovo il login.')),
);
return;
}
final uri = Uri.parse('${Config.apiUrl}/files/${file.id}');
final response = await http.delete(
uri,
headers: {'Authorization': 'Bearer $jwt'},
);
if (response.statusCode == 204) {
setState(() {
_files.removeWhere((f) => f.id == file.id);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('File ${file.name} eliminato con successo!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Errore durante l\'eliminazione.')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore di rete: ${e.toString()}')),
);
}
}
void _showShareDialog(FileInfo file) {
showDialog(
context: context,
builder: (context) => ShareDialog(
fileId: file.id,
fileName: file.name,
),
);
}
void _showFileOptions(FileInfo file) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.download),
title: const Text('Download'),
onTap: () {
Navigator.pop(context);
_downloadFile(file);
},
),
ListTile(
leading: const Icon(Icons.share),
title: const Text('Condividi'),
onTap: () {
Navigator.pop(context);
_showShareDialog(file);
},
),
ListTile(
leading: Icon(Icons.delete, color: Colors.red[700]),
title: Text('Elimina', style: TextStyle(color: Colors.red[700])),
onTap: () {
Navigator.pop(context);
_deleteFile(file);
},
),
],
),
),
);
}
Widget _buildMyFiles() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_error!, style: TextStyle(color: Colors.red[700])),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchFiles,
child: const Text('Riprova'),
),
],
),
);
}
if (_files.isEmpty) {
return const Center(
child: Text('Nessun file trovato. Carica il tuo primo file!'),
);
}
return ListView.separated(
padding: const EdgeInsets.all(32),
itemCount: _files.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, idx) {
final file = _files[idx];
return ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(file.name),
subtitle: Text(
"Size: ${(file.size / 1024).toStringAsFixed(1)} KB • Uploaded: ${file.uploadedAt.toLocal()}",
style: const TextStyle(fontSize: 12),
),
trailing: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () => _showFileOptions(file),
),
);
},
);
}
@override
Widget build(BuildContext context) {
Widget content;
if (_selectedPage == SidebarPage.myFiles) {
content = Stack(
children: [
_buildMyFiles(),
Positioned(
bottom: 32,
right: 32,
child: FloatingActionButton.extended(
onPressed: _uploadFile,
icon: const Icon(Icons.upload_file),
label: const Text("Upload"),
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
),
],
);
} else if (_selectedPage == SidebarPage.shared) {
content = const SharesPage();
} else {
content = const Center(child: Text("Settings (coming soon)"));
}
return Scaffold(
body: Row(
children: [
Sidebar(
selectedPage: _selectedPage,
onPageSelected: _onPageSelected,
onLogout: _logout,
),
Expanded(child: content),
],
),
);
}
}

View file

@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
import 'config.dart';
class ShareDialog extends StatefulWidget {
final int fileId;
final String fileName;
const ShareDialog({
required this.fileId,
required this.fileName,
super.key,
});
@override
State<ShareDialog> createState() => _ShareDialogState();
}
class _ShareDialogState extends State<ShareDialog> {
bool _isPublicShare = true;
int _expirationDays = 7;
bool _isLoading = false;
String? shareLink;
String? _error;
Future<void> _createShare() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final prefs = await SharedPreferences.getInstance();
final jwt = prefs.getString('jwt');
if (jwt == null) {
setState(() {
_error = 'Sessione scaduta, effettua di nuovo il login.';
_isLoading = false;
});
return;
}
final uri = Uri.parse('${Config.apiUrl}/share');
final response = await http.post(
uri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwt',
},
body: jsonEncode({
'file_id': widget.fileId,
'expires_in_days': _isPublicShare ? _expirationDays : null,
}),
);
if (response.statusCode == 200) {
final shareId = jsonDecode(response.body);
setState(() {
shareLink = '${Config.apiUrl}/shared/$shareId';
_isLoading = false;
});
} else {
setState(() {
_error = 'Errore durante la creazione del link di condivisione.';
_isLoading = false;
});
}
} catch (e) {
setState(() {
_error = 'Errore di rete: ${e.toString()}';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Condividi "${widget.fileName}"'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: const Text('Link pubblico'),
subtitle: const Text('Chiunque con il link può accedere al file'),
value: _isPublicShare,
onChanged: (value) {
setState(() {
_isPublicShare = value;
});
},
),
if (_isPublicShare) ...[
const SizedBox(height: 16),
const Text('Scadenza del link:'),
Slider(
value: _expirationDays.toDouble(),
min: 1,
max: 30,
divisions: 29,
label: _expirationDays.toString(),
onChanged: (value) {
setState(() {
_expirationDays = value.round();
});
},
),
Text('Il link scadrà dopo $_expirationDays giorni'),
],
if (shareLink != null) ...[
const SizedBox(height: 16),
const Text('Link di condivisione:'),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Expanded(
child: Text(
shareLink!,
style: const TextStyle(fontFamily: 'monospace'),
),
),
IconButton(
icon: const Icon(Icons.copy),
tooltip: 'Copia link',
onPressed: () {
Clipboard.setData(ClipboardData(text: shareLink ?? ''));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Link copiato negli appunti')),
);
},
),
],
),
),
],
if (_error != null) ...[
const SizedBox(height: 16),
Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Chiudi'),
),
if (shareLink == null)
ElevatedButton(
onPressed: _isLoading ? null : _createShare,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Crea link'),
),
],
);
}
}

View file

@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:file_saver/file_saver.dart';
import 'dart:io';
class SharedFile {
final String id;
final int fileId;
final String fileName;
final DateTime createdAt;
final DateTime? expiresAt;
SharedFile({
required this.id,
required this.fileId,
required this.fileName,
required this.createdAt,
this.expiresAt,
});
}
class SharesPage extends StatefulWidget {
const SharesPage({super.key});
@override
State<SharesPage> createState() => _SharesPageState();
}
class _SharesPageState extends State<SharesPage> {
List<SharedFile> _sharedFiles = [];
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_fetchSharedFiles();
}
Future<void> _fetchSharedFiles() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final prefs = await SharedPreferences.getInstance();
final jwt = prefs.getString('jwt');
if (jwt == null) {
setState(() {
_error = 'Sessione scaduta, effettua di nuovo il login.';
_isLoading = false;
});
return;
}
final uri = Uri.parse('http://localhost:8082/api/shares');
final response = await http.get(
uri,
headers: {'Authorization': 'Bearer $jwt'},
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
setState(() {
_sharedFiles = data
.map((share) => SharedFile(
id: share['id'],
fileId: share['file_id'],
fileName: share['file_name'],
createdAt: DateTime.parse(share['created_at']),
expiresAt: share['expires_at'] != null
? DateTime.parse(share['expires_at'])
: null,
))
.toList();
_isLoading = false;
});
} else {
setState(() {
_error = 'Errore durante il recupero dei file condivisi.';
_isLoading = false;
});
}
} catch (e) {
setState(() {
_error = 'Errore di rete: ${e.toString()}';
_isLoading = false;
});
}
}
Future<void> _downloadSharedFile(SharedFile share) async {
try {
final shareLink = 'http://localhost:8082/api/shared/${share.id}';
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Download in corso...')),
);
final response = await http.get(Uri.parse(shareLink));
if (response.statusCode == 200) {
// Save the file to disk
if (Platform.isAndroid || Platform.isIOS) {
// Mobile platforms
await FileSaver.instance.saveFile(
name: share.fileName,
bytes: response.bodyBytes,
);
} else {
// Desktop platforms
final directory = await getDownloadsDirectory();
if (directory != null) {
final filePath = '${directory.path}/${share.fileName}';
final fileObj = File(filePath);
await fileObj.writeAsBytes(response.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('File salvato in: $filePath')),
);
} else {
throw Exception('Could not access downloads directory');
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('File ${share.fileName} scaricato con successo!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Errore durante il download.')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore di rete: ${e.toString()}')),
);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_error!, style: TextStyle(color: Colors.red[700])),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _fetchSharedFiles,
child: const Text('Riprova'),
),
],
),
);
}
if (_sharedFiles.isEmpty) {
return const Center(
child: Text(
'Nessun file condiviso. Condividi un file dalla sezione "My Files"!'),
);
}
return ListView.separated(
padding: const EdgeInsets.all(32),
itemCount: _sharedFiles.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, idx) {
final share = _sharedFiles[idx];
return ListTile(
leading: const Icon(Icons.link),
title: Text(share.fileName),
subtitle: Text(
"Creato: ${share.createdAt.toLocal()}${share.expiresAt != null ? ' • Scade: ${share.expiresAt!.toLocal()}' : ' • Non scade mai'}",
style: const TextStyle(fontSize: 12),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.download),
tooltip: 'Scarica file',
onPressed: () => _downloadSharedFile(share),
),
IconButton(
icon: const Icon(Icons.copy),
tooltip: 'Copia link',
onPressed: () {
final shareLink =
'http://localhost:8082/api/shared/${share.id}';
Clipboard.setData(ClipboardData(text: shareLink));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Link copiato negli appunti')),
);
},
),
],
),
);
},
);
}
}

View file

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
enum SidebarPage { myFiles, shared, settings }
class Sidebar extends StatelessWidget {
final SidebarPage selectedPage;
final ValueChanged<SidebarPage> onPageSelected;
final VoidCallback onLogout;
const Sidebar({
required this.selectedPage,
required this.onPageSelected,
required this.onLogout,
super.key,
});
@override
Widget build(BuildContext context) {
final selectedColor =
Theme.of(context).colorScheme.primary.withOpacity(0.15);
final outlineColor = Colors.purple[100];
Widget buildTile(
{required IconData icon,
required String label,
required SidebarPage page}) {
final selected = selectedPage == page;
return Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: selected
? BoxDecoration(
color: selectedColor,
border: Border.all(color: outlineColor!, width: 2),
borderRadius: BorderRadius.circular(12),
)
: null,
child: ListTile(
leading: Icon(icon,
color: selected ? Theme.of(context).colorScheme.primary : null),
title: Text(label,
style: TextStyle(
color:
selected ? Theme.of(context).colorScheme.primary : null)),
onTap: () => onPageSelected(page),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
);
}
return NavigationDrawer(
backgroundColor: Theme.of(context).colorScheme.surface,
children: [
const SizedBox(height: 24),
buildTile(
icon: Icons.folder, label: 'My Files', page: SidebarPage.myFiles),
buildTile(
icon: Icons.share, label: 'Shared Files', page: SidebarPage.shared),
buildTile(
icon: Icons.settings,
label: 'Settings',
page: SidebarPage.settings),
const Spacer(),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Logout'),
onTap: onLogout,
),
const SizedBox(height: 20),
],
);
}
}

View file

@ -21,12 +21,6 @@ version: 1.0.0+1
environment:
sdk: '>=3.4.3 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
@ -35,16 +29,17 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
shared_preferences: ^2.3.3
http: ^1.4.0
file_picker: ^8.3.2
path_provider: ^2.1.5
file_saver: ^0.2.14
flutter_dotenv: ^5.2.1
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
@ -59,9 +54,9 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- .env
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware