Skip to content

Commit

Permalink
Msoej apps thumbnail 1709 (BasedHardware#1800)
Browse files Browse the repository at this point in the history
## Deploy plan
- [ ] merge PR
- [ ] create new gcp bucket for the apps thumbnail
- [ ] add new env var to backend: BUCKET_APP_THUMBNAILS, ref to gcp
bucket for the apps thumbnail
- [ ] deploy backend
- [ ] deploy the app
  • Loading branch information
beastoin authored Feb 14, 2025
2 parents a30a225 + e1e057a commit a70cf08
Show file tree
Hide file tree
Showing 11 changed files with 596 additions and 26 deletions.
50 changes: 42 additions & 8 deletions app/lib/backend/http/api/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,34 @@ Future<bool> reviewApp(String appId, AppReview review) async {
}
}

Future<Map<String, String>> uploadAppThumbnail(File file) async {
var request = http.MultipartRequest(
'POST',
Uri.parse('${Env.apiBaseUrl}v1/app/thumbnails'),
);
request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
request.headers.addAll({'Authorization': await getAuthHeader()});

try {
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);

if (response.statusCode == 200) {
var data = jsonDecode(response.body);
return {
'thumbnail_url': data['thumbnail_url'],
'thumbnail_id': data['thumbnail_id'],
};
} else {
debugPrint('Failed to upload thumbnail. Status code: ${response.statusCode}');
return {};
}
} catch (e) {
debugPrint('An error occurred uploading thumbnail: $e');
return {};
}
}

Future<bool> updateAppReview(String appId, AppReview review) async {
try {
var response = await makeApiCall(
Expand Down Expand Up @@ -157,33 +185,39 @@ Future<bool> isAppSetupCompleted(String? url) async {
}
}

Future<(bool, String)> submitAppServer(File file, Map<String, dynamic> appData) async {
Future<(bool, String, String?)> submitAppServer(File file, Map<String, dynamic> appData) async {
var request = http.MultipartRequest(
'POST',
Uri.parse('${Env.apiBaseUrl}v1/apps'),
);
request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
request.headers.addAll({'Authorization': await getAuthHeader()});
request.fields.addAll({'app_data': jsonEncode(appData)});
print(jsonEncode(appData));
debugPrint(jsonEncode(appData));
try {
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);

if (response.statusCode == 200) {
debugPrint('submitAppServer Response body: ${jsonDecode(response.body)}');
return (true, '');
var respData = jsonDecode(response.body);
String? appId = respData['app_id'];
debugPrint('submitAppServer Response body: ${respData}');
return (true, '', appId);
} else {
debugPrint('Failed to submit app. Status code: ${response.statusCode}');
if (response.body.isNotEmpty) {
return (false, jsonDecode(response.body)['detail'] as String);
return (
false,
jsonDecode(response.body)['detail'] as String,
null,
);
} else {
return (false, 'Failed to submit app. Please try again later');
return (false, 'Failed to submit app. Please try again later', '');
}
}
} catch (e) {
debugPrint('An error occurred submitAppServer: $e');
return (false, 'Failed to submit app. Please try again later');
return (false, 'Failed to submit app. Please try again later', null);
}
}

Expand All @@ -197,7 +231,7 @@ Future<bool> updateAppServer(File? file, Map<String, dynamic> appData) async {
}
request.headers.addAll({'Authorization': await getAuthHeader()});
request.fields.addAll({'app_data': jsonEncode(appData)});
print(jsonEncode(appData));
debugPrint(jsonEncode(appData));
try {
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);
Expand Down
6 changes: 6 additions & 0 deletions app/lib/backend/schema/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ class App {
double? price;
bool isUserPaid;
String? paymentLink;
List<String> thumbnailIds;
List<String> thumbnailUrls;

App({
required this.id,
Expand Down Expand Up @@ -221,6 +223,8 @@ class App {
this.price,
required this.isUserPaid,
this.paymentLink,
this.thumbnailIds = const [],
this.thumbnailUrls = const [],
});

String? getRatingAvg() => ratingAvg?.toStringAsFixed(1);
Expand Down Expand Up @@ -268,6 +272,8 @@ class App {
price: json['price'] ?? 0.0,
isUserPaid: json['is_user_paid'] ?? false,
paymentLink: json['payment_link'],
thumbnailIds: (json['thumbnails'] as List<dynamic>?)?.cast<String>() ?? [],
thumbnailUrls: (json['thumbnail_urls'] as List<dynamic>?)?.cast<String>() ?? [],
);
}

Expand Down
159 changes: 153 additions & 6 deletions app/lib/pages/apps/add_app.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:shimmer/shimmer.dart';
import 'package:friend_private/backend/preferences.dart';
import 'package:friend_private/pages/apps/widgets/full_screen_image_viewer.dart';
import 'package:friend_private/backend/schema/app.dart';
import 'package:friend_private/pages/apps/app_detail/app_detail.dart';
import 'package:friend_private/pages/apps/providers/add_app_provider.dart';
import 'package:friend_private/pages/apps/widgets/app_metadata_widget.dart';
import 'package:friend_private/pages/apps/widgets/external_trigger_fields_widget.dart';
import 'package:friend_private/pages/apps/widgets/notification_scopes_chips_widget.dart';
import 'package:friend_private/pages/apps/widgets/payment_details_widget.dart';
import 'package:friend_private/providers/app_provider.dart';
import 'package:friend_private/utils/analytics/mixpanel.dart';
import 'package:friend_private/utils/other/temp.dart';
import 'package:friend_private/widgets/confirmation_dialog.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
Expand Down Expand Up @@ -97,9 +104,7 @@ class _AddAppPageState extends State<AddAppPage> {
),
),
),
const SizedBox(
height: 18,
),
const SizedBox(height: 18),
AppMetadataWidget(
pickImage: () async {
await provider.pickImage();
Expand All @@ -122,9 +127,143 @@ class _AddAppPageState extends State<AddAppPage> {
paymentPlan: provider.mapPaymentPlanIdToName(provider.selectePaymentPlan),
)
: const SizedBox.shrink(),
const SizedBox(
height: 12,
const SizedBox(height: 18),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.all(14.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'Preview and Screenshots',
style: TextStyle(color: Colors.grey.shade300, fontSize: 16),
),
),
const SizedBox(height: 12),
SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: provider.thumbnailUrls.length + 1,
itemBuilder: (context, index) {
// Calculate dimensions to maintain 2:3 ratio
final width = 120.0;
final height = width * 1.5; // 2:3 ratio

if (index == provider.thumbnailUrls.length) {
return GestureDetector(
onTap: provider.isUploadingThumbnail ? null : provider.pickThumbnail,
child: Container(
width: width,
height: height,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(8),
),
child: provider.isUploadingThumbnail
? Shimmer.fromColors(
baseColor: Colors.grey[900]!,
highlightColor: Colors.grey[800]!,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.photo, size: 32),
),
)
: const Icon(Icons.add_photo_alternate_outlined, size: 32),
),
);
}
return Stack(
children: [
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FullScreenImageViewer(
imageUrl: provider.thumbnailUrls[index],
),
),
);
},
child: CachedNetworkImage(
imageUrl: provider.thumbnailUrls[index],
imageBuilder: (context, imageProvider) => Container(
width: 120,
height: 180, // 2:3 ratio (120 * 1.5)
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF424242),
width: 1,
),
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
placeholder: (context, url) => Shimmer.fromColors(
baseColor: Colors.grey[900]!,
highlightColor: Colors.grey[800]!,
child: Container(
width: 120,
height: 180,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
),
),
),
errorWidget: (context, url, error) => Container(
width: 120,
height: 180,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.error),
),
),
),
Positioned(
top: 4,
right: 12,
child: GestureDetector(
onTap: () => provider.removeThumbnail(index),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle,
),
child: const Icon(Icons.close, size: 16),
),
),
),
],
);
},
),
),
],
),
),
const SizedBox(height: 18),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade900,
Expand Down Expand Up @@ -338,7 +477,15 @@ class _AddAppPageState extends State<AddAppPage> {
}
SharedPreferencesUtil().showSubmitAppConfirmation = showSubmitAppConfirmation;
Navigator.pop(context);
await provider.submitApp();
String? appId = await provider.submitApp();
App? app;
if (appId != null) {
app = await context.read<AppProvider>().getAppFromId(appId);
}
if (app != null && mounted && context.mounted) {
Navigator.pop(context);
routeToPage(context, AppDetailPage(app: app));
}
},
onCancel: () {
Navigator.pop(context);
Expand Down
Loading

0 comments on commit a70cf08

Please sign in to comment.