diff --git a/firebase.json b/firebase.json index 9df8d43..c389860 100644 --- a/firebase.json +++ b/firebase.json @@ -6,10 +6,16 @@ "ui": { "enabled": true }, - "singleProjectMode": true + "singleProjectMode": true, + "storage": { + "port": 9199 + } }, "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" + }, + "storage": { + "rules": "storage.rules" } } diff --git a/lib/features/posts/data/firestorage/firestorage_event.dart b/lib/features/posts/data/firestorage/firestorage_event.dart new file mode 100644 index 0000000..4187da6 --- /dev/null +++ b/lib/features/posts/data/firestorage/firestorage_event.dart @@ -0,0 +1,40 @@ +import 'package:firebase_storage/firebase_storage.dart'; + +class FirestorageEventPaused extends FirestorageEvent { + const FirestorageEventPaused(super.event); +} + +class FirestorageEventRunning extends FirestorageEvent { + const FirestorageEventRunning(super.event); +} + +class FirestorageEventSuccess extends FirestorageEvent { + const FirestorageEventSuccess(super.event); + Future get downloadURL => _event.ref.getDownloadURL(); +} + +class FirestorageEventCanceled extends FirestorageEvent { + const FirestorageEventCanceled(super.event); +} + +class FirestorageEventError extends FirestorageEvent { + const FirestorageEventError(super.event); +} + +sealed class FirestorageEvent { + const FirestorageEvent(this._event); + final TaskSnapshot _event; + int get bytesTransferred => _event.bytesTransferred; + int get totalBytes => _event.totalBytes; + + factory FirestorageEvent.fromSnapshot(TaskSnapshot event) => switch (event) { + TaskSnapshot(state: TaskState.paused) => FirestorageEventPaused(event), + TaskSnapshot(state: TaskState.running) => + FirestorageEventRunning(event), + TaskSnapshot(state: TaskState.success) => + FirestorageEventSuccess(event), + TaskSnapshot(state: TaskState.canceled) => + FirestorageEventCanceled(event), + TaskSnapshot(state: TaskState.error) => FirestorageEventError(event), + }; +} diff --git a/lib/features/posts/data/firestorage/posts_firestorage.dart b/lib/features/posts/data/firestorage/posts_firestorage.dart new file mode 100644 index 0000000..3ee9742 --- /dev/null +++ b/lib/features/posts/data/firestorage/posts_firestorage.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:instreal/features/posts/data/firestorage/firestorage_event.dart'; + +abstract interface class PostsFirestorage { + Stream upload(File image); +} + +class PostsFirestorageImpl implements PostsFirestorage { + PostsFirestorageImpl({FirebaseStorage? storage}) + : storageRef = (storage ?? FirebaseStorage.instance).ref("posts"); + + final Reference storageRef; + + @override + Stream upload(File image) { + final task = storageRef.child('image.jpg').putFile(image); + return task.snapshotEvents.map( + (event) => FirestorageEvent.fromSnapshot(event), + ); + } +} diff --git a/lib/features/posts/post_firestore_model.dart b/lib/features/posts/data/firestore/post_firestore_model.dart similarity index 100% rename from lib/features/posts/post_firestore_model.dart rename to lib/features/posts/data/firestore/post_firestore_model.dart diff --git a/lib/features/posts/posts_firestore.dart b/lib/features/posts/data/firestore/posts_firestore.dart similarity index 92% rename from lib/features/posts/posts_firestore.dart rename to lib/features/posts/data/firestore/posts_firestore.dart index 05ca22a..4cb036f 100644 --- a/lib/features/posts/posts_firestore.dart +++ b/lib/features/posts/data/firestore/posts_firestore.dart @@ -1,5 +1,5 @@ import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:instreal/features/posts/post_firestore_model.dart'; +import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart'; abstract interface class PostsFirestore { Future> get posts; diff --git a/lib/features/posts/post_entity.dart b/lib/features/posts/post_entity.dart index a282b4f..97cef09 100644 --- a/lib/features/posts/post_entity.dart +++ b/lib/features/posts/post_entity.dart @@ -1,16 +1,41 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; @immutable -class Post { +class Post extends PostEntity { final String id; - final String title; - final String author; final String imageUrl; const Post({ required this.id, - required this.title, - required this.author, + required super.title, + required super.author, required this.imageUrl, }); } + +@immutable +class PostDraft extends PostEntity { + final File imageFile; + + const PostDraft({ + required super.title, + required super.author, + required this.imageFile, + }); + + Post toPost(String imageUrl) => Post( + id: "", + title: title, + author: author, + imageUrl: imageUrl, + ); +} + +@immutable +sealed class PostEntity { + final String title; + final String author; + const PostEntity({required this.title, required this.author}); +} diff --git a/lib/features/posts/posts_repository.dart b/lib/features/posts/posts_repository.dart index 79aa104..63f14bb 100644 --- a/lib/features/posts/posts_repository.dart +++ b/lib/features/posts/posts_repository.dart @@ -1,6 +1,7 @@ +import 'package:instreal/features/posts/data/firestorage/posts_firestorage.dart'; +import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart'; +import 'package:instreal/features/posts/data/firestore/posts_firestore.dart'; import 'package:instreal/features/posts/post_entity.dart'; -import 'package:instreal/features/posts/post_firestore_model.dart'; -import 'package:instreal/features/posts/posts_firestore.dart'; abstract interface class PostsRepository { Future> get posts; @@ -8,9 +9,10 @@ abstract interface class PostsRepository { } class PostsRepositoryImpl implements PostsRepository { - PostsRepositoryImpl({required this.firestore}); + PostsRepositoryImpl({required this.firestore, required this.firestorage}); final PostsFirestore firestore; + final PostsFirestorage firestorage; @override Future> get posts async => diff --git a/lib/main.dart b/lib/main.dart index d4540ff..8edb6f5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -16,12 +17,11 @@ void main() async { if (kDebugMode) { try { - FirebaseFirestore.instance.useFirestoreEmulator( - defaultTargetPlatform == TargetPlatform.android - ? "10.0.2.2" - : "localhost", - 8080, - ); + final emulatorHost = defaultTargetPlatform == TargetPlatform.android + ? "10.0.2.2" + : "localhost"; + FirebaseStorage.instance.useStorageEmulator(emulatorHost, 9199); + FirebaseFirestore.instance.useFirestoreEmulator(emulatorHost, 8080); } catch (e) { // ignore: avoid_print print(e); diff --git a/lib/presentation/screens/home/home.dart b/lib/presentation/screens/home/home.dart index c47bce4..63fe046 100644 --- a/lib/presentation/screens/home/home.dart +++ b/lib/presentation/screens/home/home.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:instreal/features/posts/data/firestorage/posts_firestorage.dart'; import 'package:instreal/features/posts/post_entity.dart'; -import 'package:instreal/features/posts/posts_firestore.dart'; +import 'package:instreal/features/posts/data/firestore/posts_firestore.dart'; import 'package:instreal/features/posts/posts_repository.dart'; import 'package:instreal/l10n/index.dart'; import 'package:instreal/presentation/components/index.dart'; @@ -14,7 +17,10 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - final postingsRepo = PostsRepositoryImpl(firestore: PostFirestoreImpl()); + final postingsRepo = PostsRepositoryImpl( + firestore: PostFirestoreImpl(), + firestorage: PostsFirestorageImpl(), + ); @override Widget build(BuildContext context) { @@ -68,13 +74,13 @@ class _MyHomePageState extends State { floatingActionButton: FloatingActionButton( onPressed: () async { await postingsRepo.add( - const Post( - id: "", + PostDraft( title: 'title', author: 'author', - imageUrl: - 'https://i1.wp.com/www.suitcasescholar.com/wp-content/uploads/2012/08/DSC_2583.jpg', - ), + imageFile: File( + 'https://i1.wp.com/www.suitcasescholar.com/wp-content/uploads/2012/08/DSC_2583.jpg', + ), + ).toPost('imageUrl'), ); setState(() {}); }, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6d06bd4..56786f5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,12 @@ import Foundation import cloud_firestore import firebase_core +import firebase_storage import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 5865055..5825bd5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.0" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: "195ae2b623ba054b9be3934c79bf6282831ca15a81cfd392841feea930e50253" + url: "https://pub.dev" + source: hosted + version: "11.2.4" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: be0f4254cae3ccaefd5d1435b82c1fa3bda33187387b241c55b27d7516ea9b93 + url: "https://pub.dev" + source: hosted + version: "4.4.3" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: f02c63c871deb36180b39a5fab1ee5b2677a01fa0fb642746ca62ae7f9d1a482 + url: "https://pub.dev" + source: hosted + version: "3.6.4" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 5c6a284..0f8d01a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: google_fonts: ^5.0.0 cloud_firestore: ^4.8.1 firebase_core: ^2.14.0 + firebase_storage: ^11.2.4 dev_dependencies: flutter_test: diff --git a/storage.rules b/storage.rules new file mode 100644 index 0000000..f08744f --- /dev/null +++ b/storage.rules @@ -0,0 +1,12 @@ +rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } +} diff --git a/test/features/postings/post_firestore_model_test.dart b/test/features/posts/data/firestore/post_firestore_model_test.dart similarity index 90% rename from test/features/postings/post_firestore_model_test.dart rename to test/features/posts/data/firestore/post_firestore_model_test.dart index 61db1f0..cb7f291 100644 --- a/test/features/postings/post_firestore_model_test.dart +++ b/test/features/posts/data/firestore/post_firestore_model_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:instreal/features/posts/post_firestore_model.dart'; +import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart'; void main() { test('convert json to model', () async { diff --git a/test/features/postings/posts_repository_test.dart b/test/features/posts/posts_repository_test.dart similarity index 61% rename from test/features/postings/posts_repository_test.dart rename to test/features/posts/posts_repository_test.dart index b278986..707db11 100644 --- a/test/features/postings/posts_repository_test.dart +++ b/test/features/posts/posts_repository_test.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + import 'package:flutter_test/flutter_test.dart'; -import 'package:instreal/features/posts/post_firestore_model.dart'; -import 'package:instreal/features/posts/posts_firestore.dart'; +import 'package:instreal/features/posts/data/firestorage/firestorage_event.dart'; +import 'package:instreal/features/posts/data/firestorage/posts_firestorage.dart'; +import 'package:instreal/features/posts/data/firestore/post_firestore_model.dart'; +import 'package:instreal/features/posts/data/firestore/posts_firestore.dart'; import 'package:instreal/features/posts/posts_repository.dart'; class MockFirestore implements PostsFirestore { @@ -26,16 +30,27 @@ class MockFirestore implements PostsFirestore { } } +class MockFireStorage implements PostsFirestorage { + @override + Stream upload(File image) { + throw UnimplementedError(); + } +} + void main() { test('convert model to entity', () async { final mockStore = MockFirestore(); + final mockStorage = MockFireStorage(); final mockModelData = PostFirestoreModel.fromDocument({ 'title': 'title', 'author': 'author', 'imageUrl': 'imageUrl', }, 'id'); mockStore.add(mockModelData); - final repo = PostsRepositoryImpl(firestore: mockStore); + final repo = PostsRepositoryImpl( + firestore: mockStore, + firestorage: mockStorage, + ); final posts = await repo.posts; expect(posts.length, 1); expect(posts[0].id, 'id');