diff --git a/api/src/main.rs b/api/src/main.rs index 9cb65cd..6a1b0cc 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -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> { 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?; diff --git a/lightcloud_app/analysis_options.yaml b/lightcloud_app/analysis_options.yaml index 0d29021..fc8e8a3 100644 --- a/lightcloud_app/analysis_options.yaml +++ b/lightcloud_app/analysis_options.yaml @@ -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: diff --git a/lightcloud_app/lib/config.dart b/lightcloud_app/lib/config.dart new file mode 100644 index 0000000..b0aa695 --- /dev/null +++ b/lightcloud_app/lib/config.dart @@ -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'; +} diff --git a/lightcloud_app/lib/main.dart b/lightcloud_app/lib/main.dart index c6938f8..3a67d72 100644 --- a/lightcloud_app/lib/main.dart +++ b/lightcloud_app/lib/main.dart @@ -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 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( + 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 { 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 _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 { 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 { ), 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 { 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 { ), 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, diff --git a/lightcloud_app/lib/mainpage.dart b/lightcloud_app/lib/mainpage.dart new file mode 100644 index 0000000..a1fa240 --- /dev/null +++ b/lightcloud_app/lib/mainpage.dart @@ -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 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 createState() => _MainPageState(); +} + +class _MainPageState extends State { + SidebarPage _selectedPage = SidebarPage.myFiles; + List _files = []; + bool _isLoading = false; + String? _error; + + @override + void initState() { + super.initState(); + _fetchFiles(); + } + + Future _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 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 _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 _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 _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 _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), + ], + ), + ); + } +} diff --git a/lightcloud_app/lib/sharedialog.dart b/lightcloud_app/lib/sharedialog.dart new file mode 100644 index 0000000..7698b30 --- /dev/null +++ b/lightcloud_app/lib/sharedialog.dart @@ -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 createState() => _ShareDialogState(); +} + +class _ShareDialogState extends State { + bool _isPublicShare = true; + int _expirationDays = 7; + bool _isLoading = false; + String? shareLink; + String? _error; + + Future _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'), + ), + ], + ); + } +} diff --git a/lightcloud_app/lib/shares.dart b/lightcloud_app/lib/shares.dart new file mode 100644 index 0000000..b1b4ec7 --- /dev/null +++ b/lightcloud_app/lib/shares.dart @@ -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 createState() => _SharesPageState(); +} + +class _SharesPageState extends State { + List _sharedFiles = []; + bool _isLoading = false; + String? _error; + + @override + void initState() { + super.initState(); + _fetchSharedFiles(); + } + + Future _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 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 _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')), + ); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lightcloud_app/lib/sidebar.dart b/lightcloud_app/lib/sidebar.dart new file mode 100644 index 0000000..a447c02 --- /dev/null +++ b/lightcloud_app/lib/sidebar.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +enum SidebarPage { myFiles, shared, settings } + +class Sidebar extends StatelessWidget { + final SidebarPage selectedPage; + final ValueChanged 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), + ], + ); + } +} diff --git a/lightcloud_app/pubspec.yaml b/lightcloud_app/pubspec.yaml index 32c5890..33b49ff 100644 --- a/lightcloud_app/pubspec.yaml +++ b/lightcloud_app/pubspec.yaml @@ -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