19 Commits

Author SHA1 Message Date
807ece57dd 🚀 Launch SolarAgent Technology Preview 2024-03-24 23:15:01 +08:00
91d238bc2f Post details
💄 Somewhere optimization
2024-03-24 22:57:12 +08:00
217c20164f Render article in feed 2024-03-24 22:23:23 +08:00
52304a7633 Upload photos 2024-03-24 21:55:20 +08:00
a303f5c30c 🐛 Fix and optimize something 2024-03-24 20:51:12 +08:00
92354eee34 📈 Add sentry 2024-03-24 20:09:50 +08:00
c3df9b0f5b 🚀 Launch v2.0.0+1 2024-03-24 20:08:16 +08:00
3705ab82f3 💄 Optimized menu 2024-03-24 16:08:51 +08:00
f6429115a4 Add post toolbar 2024-03-24 16:07:29 +08:00
527655458f 🐛 Fix notify dismiss 2024-03-24 15:52:32 +08:00
b6a11dc858 💄 Optimized safe area 2024-03-24 15:49:20 +08:00
b520f27b90 💩 Kinda rude to solve token doesn't refresh 2024-03-24 13:43:23 +08:00
7c8c1025e2 Reactions 2024-03-24 13:34:29 +08:00
cba4f19b17 Reload after post 2024-03-24 12:32:24 +08:00
eb01942e2d Comment post 2024-03-24 12:22:29 +08:00
c4157c3e23 Comment view 2024-03-24 12:12:13 +08:00
b32a7216b5 Pull to refresh 2024-03-24 11:35:18 +08:00
e74aba2d8b 💄 Optimized image browsing 2024-03-24 11:20:59 +08:00
41f456893f 🐛 Bug fixes 2024-03-24 11:07:36 +08:00
72 changed files with 1325 additions and 180 deletions

View File

@@ -1,39 +0,0 @@
{
"project_info": {
"project_number": "659822066072",
"project_id": "smartsheep-hydrogen",
"storage_bucket": "smartsheep-hydrogen.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:659822066072:android:39e699282c97a7cfc013ed",
"android_client_info": {
"package_name": "dev.solsynth.solaragent"
}
},
"oauth_client": [
{
"client_id": "659822066072-dde0aqiocn28bc1gk9p5k8oaqe1jpi0l.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBLPaAK4CVW9umXIdUoGOGHO42jKnwZkKo"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "659822066072-dde0aqiocn28bc1gk9p5k8oaqe1jpi0l.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="SolarAgent"
android:label="Solian"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -1,39 +0,0 @@
{
"project_info": {
"project_number": "659822066072",
"project_id": "smartsheep-hydrogen",
"storage_bucket": "smartsheep-hydrogen.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:659822066072:android:39e699282c97a7cfc013ed",
"android_client_info": {
"package_name": "dev.solsynth.solaragent"
}
},
"oauth_client": [
{
"client_id": "659822066072-dde0aqiocn28bc1gk9p5k8oaqe1jpi0l.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBLPaAK4CVW9umXIdUoGOGHO42jKnwZkKo"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "659822066072-dde0aqiocn28bc1gk9p5k8oaqe1jpi0l.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

BIN
image.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBQB4u2KKe1P5jMG_zWGiUFtpcjQKhG3jY</string>
<key>GCM_SENDER_ID</key>
<string>659822066072</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>dev.solsynth.solaragent</string>
<key>PROJECT_ID</key>
<string>smartsheep-hydrogen</string>
<key>STORAGE_BUCKET</key>
<string>smartsheep-hydrogen.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:659822066072:ios:90dff099ef47fc8fc013ed</string>
</dict>
</plist>

View File

@@ -1,38 +1,103 @@
PODS:
- DKImagePickerController/Core (4.3.4):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.4)
- DKImagePickerController/PhotoGallery (4.3.4):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.4)
- DKPhotoGallery (0.0.17):
- DKPhotoGallery/Core (= 0.0.17)
- DKPhotoGallery/Model (= 0.0.17)
- DKPhotoGallery/Preview (= 0.0.17)
- DKPhotoGallery/Resource (= 0.0.17)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.17):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.17):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- SDWebImage (5.19.0):
- SDWebImage/Core (= 5.19.0)
- SDWebImage/Core (5.19.0)
- Sentry/HybridSDK (8.21.0):
- SentryPrivate (= 8.21.0)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.21.0)
- SentryPrivate (8.21.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.4)
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- SDWebImage
- Sentry
- SentryPrivate
- SwiftyGif
EXTERNAL SOURCES:
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
@@ -41,11 +106,20 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
SDWebImage: 981fd7e860af070920f249fd092420006014c3eb
Sentry: ebc12276bd17613a114ab359074096b6b3725203
sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e
SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36

View File

@@ -477,7 +477,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolarAgent;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -666,7 +666,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolarAgent;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -693,7 +693,7 @@
DEVELOPMENT_TEAM = W7HPZ53V6B;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SolarAgent;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "LaunchImage.png",
"filename" : "iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait 2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "LaunchImage@2x.png",
"filename" : "iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Launch Screen.png",
"filename" : "iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -7,7 +7,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>SolarAgent</string>
<string>Solian</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -15,7 +15,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>SolarAgent</string>
<string>Solian</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -29,10 +29,10 @@
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>sms</string>
<string>tel</string>
</array>
<array>
<string>sms</string>
<string>tel</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
@@ -55,5 +55,11 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCameraUsageDescription</key>
<string>Allow Solar access to your camera to take photo, so that you can attach image in your post.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow Solar access to your microphone to record audio, so that you can attach image in your post.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow Solar access to your photo library, so that you can attach image in your post.</string>
</dict>
</plist>

View File

@@ -1,7 +0,0 @@
{
"file_generated_by": "FlutterFire CLI",
"purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory",
"GOOGLE_APP_ID": "1:659822066072:ios:90dff099ef47fc8fc013ed",
"FIREBASE_PROJECT_ID": "smartsheep-hydrogen",
"GCM_SENDER_ID": "659822066072"
}

View File

@@ -27,6 +27,7 @@ class AuthGuard {
static const profileKey = "profiles";
oauth2.Client? client;
DateTime? lastRefreshedAt;
Future<bool> pickClient() async {
if (await storage.containsKey(key: storageKey)) {
@@ -109,8 +110,14 @@ class AuthGuard {
Future<bool> isAuthorized() async {
const storage = FlutterSecureStorage();
if (await storage.containsKey(key: storageKey)) {
if (client != null && client!.credentials.isExpired) {
await refreshToken();
if (client != null) {
if (lastRefreshedAt == null ||
lastRefreshedAt!
.add(const Duration(minutes: 3))
.isAfter(DateTime.now())) {
await refreshToken();
lastRefreshedAt = DateTime.now();
}
}
return true;
} else {

View File

@@ -2,13 +2,23 @@ import 'package:flutter/material.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/router.dart';
import 'package:solaragent/widgets/navigation.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn =
'https://a629d9164a60a28ac855c52f4f543722@o4506965897117696.ingest.us.sentry.io/4506965900001280';
options.tracesSampleRate = 1.0;
},
appRunner: () async {
WidgetsFlutterBinding.ensureInitialized();
await authClient.pickClient();
await authClient.pickClient();
runApp(const SolarAgent());
runApp(const SolarAgent());
},
);
}
class SolarAgent extends StatelessWidget {
@@ -31,8 +41,10 @@ class SolarAgent extends StatelessWidget {
builder: (context, child) => Overlay(
initialEntries: [
OverlayEntry(
builder: (context) => SafeArea(
builder: (context) => Container(
color: Colors.white,
child: SafeArea(
bottom: false,
child: Scaffold(
body: child,
bottomNavigationBar: const AgentNavigation(),

View File

@@ -1,7 +1,10 @@
import 'package:go_router/go_router.dart';
import 'package:solaragent/models/feed.dart';
import 'package:solaragent/screens/account.dart';
import 'package:solaragent/screens/explore.dart';
import 'package:solaragent/screens/notifications.dart';
import 'package:solaragent/screens/posts/screen.dart';
import 'package:solaragent/screens/publish/comment_editor.dart';
import 'package:solaragent/screens/publish/moment_editor.dart';
final router = GoRouter(
@@ -20,8 +23,21 @@ final router = GoRouter(
),
GoRoute(
path: '/post/moments',
path: '/post/new/moments',
builder: (context, state) => const MomentEditorScreen(),
),
GoRoute(
path: '/post/new/comments',
builder: (context, state) =>
CommentEditorScreen(parent: state.extra as Feed),
),
GoRoute(
path: '/post/:modelType/:alias',
builder: (context, state) => PostScreen(
modelType: state.pathParameters['modelType'] as String,
alias: state.pathParameters['alias'] as String,
),
),
],
);

View File

@@ -16,7 +16,7 @@ class AboutScreen extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('SolarAgent',
Text('Solian',
style: Theme.of(context).textTheme.headlineMedium),
Text('Solar Networks Official Mobile Application',
style: Theme.of(context).textTheme.bodyMedium),

View File

@@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/models/feed.dart';
@@ -9,7 +8,6 @@ import 'package:solaragent/models/pagination.dart';
import 'package:http/http.dart' as http;
import 'package:solaragent/router.dart';
import 'package:solaragent/widgets/feed.dart';
import 'package:solaragent/screens/publish/moment_editor.dart';
class ExploreScreen extends StatefulWidget {
const ExploreScreen({super.key});
@@ -47,13 +45,14 @@ class _ExploreScreenState extends State<ExploreScreen> {
if (res.statusCode == 200) {
final result =
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
final isLastPage = (result.data?.length ?? 0) < pageSize;
if (isLastPage) {
paginationController.appendLastPage(feed);
final isLastPage = (result.count - pageKey) < pageSize;
final items =
result.data?.map((x) => Feed.fromJson(x)).toList() ?? List.empty();
if (isLastPage || result.data == null) {
paginationController.appendLastPage(items);
} else {
final feed = result.data!.map((x) => Feed.fromJson(x)).toList();
final nextPageKey = pageKey + feed.length;
paginationController.appendPage(feed, nextPageKey);
final nextPageKey = pageKey + items.length;
paginationController.appendPage(items, nextPageKey);
}
} else {
paginationController.error = utf8.decode(res.bodyBytes);
@@ -63,11 +62,17 @@ class _ExploreScreenState extends State<ExploreScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: PagedListView<int, Feed>(
pagingController: paginationController,
builderDelegate: PagedChildBuilderDelegate<Feed>(
itemBuilder: (context, item, index) => FeedItem(
item: item,
body: RefreshIndicator(
onRefresh: () async {
paginationController.refresh();
},
child: PagedListView<int, Feed>(
pagingController: paginationController,
builderDelegate: PagedChildBuilderDelegate<Feed>(
itemBuilder: (context, item, index) => FeedItem(
item: item,
brief: true,
),
),
),
),
@@ -78,7 +83,9 @@ class _ExploreScreenState extends State<ExploreScreen> {
return FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () {
router.push("/post/moments");
router.push("/post/new/moments").then((value) {
if (value == true) paginationController.refresh();
});
},
);
} else {

View File

@@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:solaragent/models/feed.dart';
import 'package:solaragent/widgets/feed.dart';
class PostScreen extends StatefulWidget {
final Client client = Client();
final String modelType;
final String alias;
PostScreen({super.key, required this.modelType, required this.alias});
@override
State<PostScreen> createState() => _PostScreenState();
}
class _PostScreenState extends State<PostScreen> {
Future<Feed?> pullPost(BuildContext context) async {
var uri = Uri.parse(
"https://co.solsynth.dev/api/p/${widget.modelType}s/${widget.alias}",
);
var res = await widget.client.get(uri);
if (res.statusCode != 200) {
var err = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $err")),
);
return null;
} else {
return Feed.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
}
}
Widget buildItem(BuildContext context, Feed item) {
return FeedItem(
item: item,
brief: false,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Post")),
body: FutureBuilder(
future: pullPost(context),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return SingleChildScrollView(
child: buildItem(context, snapshot.data!),
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}

View File

@@ -0,0 +1,212 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:image_picker/image_picker.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/models/feed.dart';
class AttachmentList extends StatefulWidget {
final List<Attachment> current;
final void Function(List<Attachment> data) onUpdate;
const AttachmentList(
{super.key, required this.current, required this.onUpdate});
@override
State<AttachmentList> createState() => _AttachmentListState();
}
class _AttachmentListState extends State<AttachmentList> {
final imagePicker = ImagePicker();
bool isSubmitting = false;
List<Attachment> attachments = List.empty(growable: true);
Future<void> pickImageToUpload(BuildContext context) async {
if (!await authClient.isAuthorized()) return;
final image = await imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
final file = File(image.path);
final hashcode = await calculateSha256(file);
try {
await uploadAttachment(file, hashcode);
} catch (err) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $err")),
);
}
}
Future<void> uploadAttachment(File file, String hashcode) async {
var req = MultipartRequest(
'POST',
Uri.parse('https://co.solsynth.dev/api/attachments'),
);
req.files.add(await MultipartFile.fromPath('attachment', file.path));
req.fields['hashcode'] = hashcode;
setState(() => isSubmitting = true);
var res = await authClient.client!.send(req);
if (res.statusCode == 200) {
var result = Attachment.fromJson(
jsonDecode(utf8.decode(await res.stream.toBytes()))["info"],
);
setState(() => attachments.add(result));
widget.onUpdate(attachments);
} else {
throw Exception(utf8.decode(await res.stream.toBytes()));
}
setState(() => isSubmitting = false);
}
Future<void> disposeAttachment(
BuildContext context, Attachment item, int index) async {
var req = MultipartRequest(
'DELETE',
Uri.parse('https://co.solsynth.dev/api/attachments/${item.id}'),
);
setState(() => isSubmitting = true);
var res = await authClient.client!.send(req);
if (res.statusCode == 200) {
setState(() => attachments.removeAt(index));
widget.onUpdate(attachments);
} else {
final err = utf8.decode(await res.stream.toBytes());
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $err")),
);
}
setState(() => isSubmitting = false);
}
Future<String> calculateSha256(File file) async {
final bytes = await file.readAsBytes();
final digest = sha256.convert(bytes);
return digest.toString();
}
String getFileName(Attachment item) {
return item.filename.replaceAll(RegExp(r'\.[^/.]+$'), '');
}
String getFileType(Attachment item) {
switch (item.type) {
case 1:
return 'Photo';
case 2:
return 'Video';
case 3:
return 'Audio';
default:
return 'Others';
}
}
String formatBytes(int bytes, {int decimals = 2}) {
if (bytes == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
@override
void initState() {
attachments = widget.current;
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(left: 10, right: 10, top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 12.0,
),
child: Text(
'Attachments',
style: Theme.of(context).textTheme.headlineSmall,
),
),
FutureBuilder(
future: authClient.isAuthorized(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return Tooltip(
message: "Add a photo",
child: TextButton(
onPressed: isSubmitting
? null
: () => pickImageToUpload(context),
style: TextButton.styleFrom(
shape: const CircleBorder(),
),
child: const Icon(Icons.add_photo_alternate),
),
);
} else {
return Container();
}
},
),
],
),
),
isSubmitting ? const LinearProgressIndicator() : Container(),
Expanded(
child: ListView.separated(
itemCount: attachments.length,
itemBuilder: (context, index) {
var element = attachments[index];
return Container(
padding: const EdgeInsets.only(left: 8.0),
child: ListTile(
title: Text(getFileName(element)),
subtitle: Text(
"${getFileType(element)} · ${formatBytes(element.filesize)}",
),
trailing: TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
foregroundColor: Colors.red,
),
child: const Icon(Icons.delete),
onPressed: () => disposeAttachment(context, element, index),
),
),
);
},
separatorBuilder: (context, index) => const Divider(),
),
),
],
);
}
}

View File

@@ -0,0 +1,169 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/models/feed.dart';
import 'package:solaragent/router.dart';
import 'package:url_launcher/url_launcher.dart';
class CommentEditorScreen extends StatefulWidget {
final Feed parent;
const CommentEditorScreen({super.key, required this.parent});
@override
State<CommentEditorScreen> createState() => _CommentEditorScreenState();
}
class _CommentEditorScreenState extends State<CommentEditorScreen> {
final contentController = TextEditingController();
bool isSubmitting = false;
bool showRecommendationBanner = true;
Future<void> postComment() async {
if (!await authClient.isAuthorized()) return;
var dataset = "${widget.parent.modelType}s";
var alias = widget.parent.alias;
var uri = Uri.parse(
"https://co.solsynth.dev/api/p/$dataset/$alias/comments",
);
setState(() => isSubmitting = true);
var res = await authClient.client!.post(
uri,
headers: <String, String>{
'Content-Type': 'application/json',
},
body: jsonEncode(<String, dynamic>{
'content': contentController.value.text,
}),
);
if (res.statusCode != 200) {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
} else {
if (router.canPop()) {
router.pop(true);
}
}
setState(() => isSubmitting = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Leave a comment"),
actions: <Widget>[
TextButton(
onPressed: !isSubmitting ? postComment : null,
child: const Text('POST'),
),
],
),
body: Column(
children: [
// Loading indicator
isSubmitting ? const LinearProgressIndicator() : Container(),
// Userinfo
FutureBuilder(
future: authClient.getProfiles(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var userinfo = snapshot.data;
return Container(
color: Colors.grey[50],
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(userinfo["nick"]),
subtitle: const Text("You will post this post as"),
leading: CircleAvatar(
backgroundImage: NetworkImage(userinfo["picture"]),
),
),
);
} else {
return Container();
}
}),
// Editor
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: contentController,
decoration: const InputDecoration.collapsed(
hintText: "What do you want to say?",
),
),
),
),
// Recommend website banner
showRecommendationBanner
? FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.data?.getBool(
"editor.hide_website_recommendation") ==
null) {
snapshot.data
?.remove("editor.hide_website_recommendation");
return MaterialBanner(
padding: const EdgeInsets.all(20),
content: const Text(
'Solian still in early stage development. Some features isn\'t available. We recommend use our website, also optimized for moblie!',
),
leading: const Icon(Icons.construction),
backgroundColor: const Color(0xFFE0E0E0),
actions: <Widget>[
TextButton(
child: const Text('OPEN'),
onPressed: () async {
await launchUrl(
Uri.parse("https://co.solsynth.dev"),
mode: LaunchMode.externalApplication,
);
},
),
TextButton(
child: const Text('DISMISS'),
onPressed: () async {
await snapshot.data?.setBool(
"editor.hide_website_recommendation",
true,
);
setState(() {
showRecommendationBanner = false;
});
},
),
],
);
} else {
return Container();
}
})
: Container(),
],
),
);
}
@override
void dispose() {
contentController.dispose();
super.dispose();
}
}

View File

@@ -1,10 +1,11 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/models/feed.dart';
import 'package:solaragent/router.dart';
import 'package:solaragent/screens/publish/attachment_list.dart';
import 'package:url_launcher/url_launcher.dart';
class MomentEditorScreen extends StatefulWidget {
@@ -21,8 +22,10 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
bool showRecommendationBanner = true;
List<Attachment> attachments = List.empty(growable: true);
Future<void> postMoment() async {
if (authClient.client == null) return;
if (!await authClient.isAuthorized()) return;
setState(() => isSubmitting = true);
var res = await authClient.client!.post(
@@ -32,6 +35,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
},
body: jsonEncode(<String, dynamic>{
'content': contentController.value.text,
'attachments': attachments,
}),
);
if (res.statusCode != 200) {
@@ -41,12 +45,22 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
);
} else {
if (router.canPop()) {
router.pop();
router.pop(true);
}
}
setState(() => isSubmitting = false);
}
void viewAttachments(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentList(
current: attachments,
onUpdate: (value) => attachments = value,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -95,10 +109,28 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
keyboardType: TextInputType.multiline,
controller: contentController,
decoration: const InputDecoration.collapsed(
hintText: "What\'s happened?!"),
hintText: "What's happened?!",
),
),
),
),
// Toolbar
Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
),
),
child: Row(
children: [
TextButton(
style: TextButton.styleFrom(shape: const CircleBorder()),
child: const Icon(Icons.camera_alt),
onPressed: () => viewAttachments(context),
)
],
),
),
// Recommend website banner
showRecommendationBanner
? FutureBuilder(
@@ -108,11 +140,12 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
snapshot.data?.getBool(
"editor.hide_website_recommendation") ==
null) {
snapshot.data?.remove("editor.hide_website_recommendation");
snapshot.data
?.remove("editor.hide_website_recommendation");
return MaterialBanner(
padding: const EdgeInsets.all(20),
content: const Text(
'SolarAgent still in early stage development. Some features isn\'t available. We recommend use our website, also optimized for moblie!',
'Solian still in early stage development. Some features isn\'t available. We recommend use our website, also optimized for moblie!',
),
leading: const Icon(Icons.construction),
backgroundColor: const Color(0xFFE0E0E0),
@@ -121,7 +154,9 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
child: const Text('OPEN'),
onPressed: () async {
await launchUrl(
Uri.parse("https://co.solsynth.dev"));
Uri.parse("https://co.solsynth.dev"),
mode: LaunchMode.externalApplication,
);
},
),
TextButton(

View File

@@ -1,16 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:solaragent/models/feed.dart';
import 'package:solaragent/router.dart';
import 'package:solaragent/widgets/image.dart';
import 'package:solaragent/widgets/posts/comments.dart';
import 'package:solaragent/widgets/posts/content/article.dart';
import 'package:solaragent/widgets/posts/content/moment.dart';
import 'package:solaragent/widgets/posts/reactions.dart';
class FeedItem extends StatelessWidget {
class FeedItem extends StatefulWidget {
final Feed item;
final bool? brief;
const FeedItem({super.key, required this.item});
const FeedItem({super.key, required this.item, this.brief});
@override
State<FeedItem> createState() => _FeedItemState();
}
class _FeedItemState extends State<FeedItem> {
int reactionCount = 0;
Map<String, dynamic>? reactionList;
void viewComments(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => CommentList(parent: widget.item),
);
}
void viewReactions(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => ReactionList(
parent: widget.item,
onReact: (symbol, num) {
setState(() {
if (!reactionList!.containsKey(symbol)) {
reactionList![symbol] = 0;
}
reactionCount += num;
reactionList![symbol] += num;
});
},
),
);
}
bool hasAttachments() =>
item.attachments != null && item.attachments!.isNotEmpty;
widget.item.modelType == "moment" &&
widget.item.attachments != null &&
widget.item.attachments!.isNotEmpty;
String getDescription(String desc) =>
desc.isEmpty ? "No description yet." : desc;
@@ -18,30 +62,34 @@ class FeedItem extends StatelessWidget {
String getFileUrl(String fileId) =>
'https://co.solsynth.dev/api/attachments/o/$fileId';
@override
Widget build(BuildContext context) {
Widget buildContent() {
switch (widget.item.modelType) {
case "article":
return ArticleContent(item: widget.item, brief: widget.brief ?? false);
default:
return MomentContent(item: widget.item, brief: widget.brief ?? false);
}
}
Widget buildItem(BuildContext context) {
return Column(
children: [
Container(
color: Colors.grey[50],
child: ListTile(
title: Text(item.author.name),
title: Text(widget.item.author.name),
leading: CircleAvatar(
backgroundImage: NetworkImage(item.author.avatar),
backgroundImage: NetworkImage(widget.item.author.avatar),
),
subtitle: Text(
getDescription(item.author.description),
getDescription(widget.item.author.description),
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
),
),
),
Markdown(
data: item.content,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
),
buildContent(),
hasAttachments()
? Container(
decoration: const BoxDecoration(
@@ -50,16 +98,16 @@ class FeedItem extends StatelessWidget {
),
child: FlutterCarousel(
options: CarouselOptions(
height: 240.0,
height: 360.0,
viewportFraction: 1.0,
showIndicator: true,
slideIndicator: const CircularSlideIndicator(),
),
items: item.attachments?.map((x) {
items: widget.item.attachments?.map((x) {
return Builder(
builder: (BuildContext context) {
return Container(
return SizedBox(
width: MediaQuery.of(context).size.width,
margin: const EdgeInsets.symmetric(horizontal: 5.0),
child: InkWell(
child: Image.network(
getFileUrl(x.fileId),
@@ -81,7 +129,49 @@ class FeedItem extends StatelessWidget {
),
)
: Container(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(width: 0.3, color: Color(0xffdedede)),
),
),
child: Row(
children: [
TextButton.icon(
icon: const Icon(Icons.comment),
label: Text(widget.item.commentCount.toString()),
onPressed: () => viewComments(context),
),
TextButton.icon(
icon: const Icon(Icons.emoji_emotions),
label: Text(reactionCount.toString()),
style: TextButton.styleFrom(foregroundColor: Colors.teal),
onPressed: () => viewReactions(context),
),
],
),
),
],
);
}
@override
void initState() {
reactionCount = widget.item.reactionCount;
reactionList = widget.item.reactionList ?? <String, dynamic>{};
super.initState();
}
@override
Widget build(BuildContext context) {
return (widget.brief ?? false)
? GestureDetector(
child: buildItem(context),
onTap: () => router.push(
"/post/${widget.item.modelType}/${widget.item.alias}",
),
)
: buildItem(context);
}
}

View File

@@ -12,6 +12,7 @@ class ImageLightbox extends StatelessWidget {
child: Center(
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: InteractiveViewer(
boundaryMargin: const EdgeInsets.all(128),
minScale: 0.1,

View File

@@ -38,9 +38,9 @@ class NotificationItem extends StatelessWidget {
);
}
Future<void> markAsRead(element) async {
Future<void> markAsRead(model.Notification element) async {
if (authClient.client != null) {
var id = element['id'];
var id = element.id;
var uri = Uri.parse('https://id.solsynth.dev/api/notifications/$id/read');
await authClient.client!.put(uri);
}

View File

@@ -0,0 +1,130 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/models/feed.dart';
import 'package:solaragent/models/pagination.dart';
import 'package:solaragent/router.dart';
import 'package:solaragent/widgets/feed.dart';
class CommentList extends StatefulWidget {
final Feed parent;
const CommentList({super.key, required this.parent});
@override
State<CommentList> createState() => _CommentListState();
}
class _CommentListState extends State<CommentList> {
static const pageSize = 5;
final client = Client();
final PagingController<int, Feed> paginationController =
PagingController(firstPageKey: 0);
List<Feed> feed = List.empty();
@override
void initState() {
super.initState();
paginationController.addPageRequestListener((pageKey) {
pullFeed(pageKey);
});
}
Future<void> pullFeed(int pageKey) async {
var offset = pageKey;
var take = pageSize;
var dataset = "${widget.parent.modelType}s";
var alias = widget.parent.alias;
var uri = Uri.parse(
'https://co.solsynth.dev/api/p/$dataset/$alias/comments?take=$take&offset=$offset',
);
var res = await client.get(uri);
if (res.statusCode == 200) {
final result =
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
final isLastPage = (result.count - pageKey) < pageSize;
final items =
result.data?.map((x) => Feed.fromJson(x)).toList() ?? List.empty();
if (isLastPage || result.data == null) {
paginationController.appendLastPage(items);
} else {
final nextPageKey = pageKey + items.length;
paginationController.appendPage(items, nextPageKey);
}
} else {
paginationController.error = utf8.decode(res.bodyBytes);
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(left: 10, right: 10, top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 12.0,
),
child: Text(
'Comments',
style: Theme.of(context).textTheme.headlineSmall,
),
),
FutureBuilder(
future: authClient.isAuthorized(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return TextButton.icon(
icon: const Icon(Icons.edit),
label: const Text("LEAVE COMMENT"),
onPressed: () {
router
.push("/post/new/comments", extra: widget.parent)
.then((value) {
if (value == true) paginationController.refresh();
});
},
);
} else {
return Container();
}
},
),
],
),
),
Expanded(
child: PagedListView<int, Feed>(
pagingController: paginationController,
builderDelegate: PagedChildBuilderDelegate<Feed>(
itemBuilder: (context, item, index) => FeedItem(
item: item,
brief: true,
),
),
),
),
],
);
}
@override
void dispose() {
paginationController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:solaragent/models/feed.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
class ArticleContent extends StatelessWidget {
final Feed item;
final bool brief;
const ArticleContent({super.key, required this.item, required this.brief});
@override
Widget build(BuildContext context) {
return brief
? ListTile(
title: Text(item.title),
subtitle: Text(item.description),
)
: Column(
children: [
ListTile(
title: Text(item.title),
subtitle: Text(item.description),
),
const Divider(color: Color(0xffefefef)),
Markdown(
selectable: !brief,
data: item.content,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
extensionSet: md.ExtensionSet(
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
md.ExtensionSet.gitHubFlavored.inlineSyntaxes,
),
onTapLink: (text, href, title) async {
if (href == null) return;
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}),
],
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:solaragent/models/feed.dart';
class MomentContent extends StatelessWidget {
final Feed item;
final bool brief;
const MomentContent({super.key, required this.brief, required this.item});
@override
Widget build(BuildContext context) {
return Markdown(
selectable: !brief,
data: item.content,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
);
}
}

View File

@@ -0,0 +1,175 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:solaragent/auth.dart';
import 'package:solaragent/models/feed.dart';
class ReactionList extends StatefulWidget {
static const Map<String, Map<String, dynamic>> emojis = {
'thumb_up': {'icon': '👍', 'attitude': 1},
'clap': {'icon': '👏', 'attitude': 1}
};
final Feed parent;
final void Function(String symbol, int num) onReact;
const ReactionList({super.key, required this.parent, required this.onReact});
@override
State<ReactionList> createState() => _ReactionListState();
}
class _ReactionListState extends State<ReactionList> {
bool isSubmitting = false;
void viewReactMenu(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) {
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: ReactionList.emojis.length,
itemBuilder: (BuildContext context, int index) {
var element = ReactionList.emojis.entries.toList()[index];
return InkWell(
borderRadius: const BorderRadius.all(
Radius.circular(64),
),
onTap: () async {
await doReact(element.key, element.value['attitude']);
},
child: ListTile(
title: Text(element.value['icon']),
subtitle: Text(
":${element.key}:",
style: const TextStyle(fontFamily: "monospace"),
),
),
);
});
},
);
}
Future<void> doReact(String symbol, int attitude) async {
if (!await authClient.isAuthorized()) return;
var dataset = "${widget.parent.modelType}s";
var alias = widget.parent.id;
var uri = Uri.parse(
"https://co.solsynth.dev/api/p/$dataset/$alias/react",
);
setState(() => isSubmitting = true);
var res = await authClient.client!.post(
uri,
headers: <String, String>{
'Content-Type': 'application/json',
},
body: jsonEncode(<String, dynamic>{
'symbol': symbol,
'attitude': attitude,
}),
);
if (res.statusCode == 201) {
widget.onReact(symbol, 1);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Your reaction has been added onto this post."),
),
);
} else if (res.statusCode == 204) {
widget.onReact(symbol, -1);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Your reaction has been removed from this post."),
),
);
} else {
var message = utf8.decode(res.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Something went wrong... $message")),
);
}
setState(() => isSubmitting = false);
}
List<MapEntry<String, dynamic>> getReactionEntries() =>
widget.parent.reactionList?.entries.toList() ?? List.empty();
@override
Widget build(BuildContext context) {
return Column(
children: [
// Title
Container(
padding: const EdgeInsets.only(left: 10, right: 10, top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 12.0,
),
child: Text(
'Reactions',
style: Theme.of(context).textTheme.headlineSmall,
),
),
FutureBuilder(
future: authClient.isAuthorized(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
return TextButton.icon(
icon: const Icon(Icons.add_reaction),
label: const Text("REACT"),
onPressed:
isSubmitting ? null : () => viewReactMenu(context),
);
} else {
return Container();
}
},
),
],
),
),
// Loading indicator
isSubmitting ? const LinearProgressIndicator() : Container(),
// Data list
Expanded(
child: ListView.separated(
itemCount: getReactionEntries().length,
itemBuilder: (BuildContext context, int index) {
var element = getReactionEntries()[index];
return InkWell(
onTap: isSubmitting
? null
: () {
doReact(
element.key,
ReactionList.emojis[element.key]!['attitude'],
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: ListTile(
title: Text(
"${ReactionList.emojis[element.key]!['icon']} x${element.value.toString()}",
),
subtitle: Text(
":${element.key}:",
style: const TextStyle(fontFamily: "monospace"),
),
),
));
},
separatorBuilder: (context, index) => const Divider(),
),
),
],
);
}
}

View File

@@ -6,13 +6,21 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <sentry_flutter/sentry_flutter_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) sentry_flutter_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin");
sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -3,7 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
sentry_flutter
url_launcher_linux
)

View File

@@ -5,16 +5,20 @@
import FlutterMacOS
import Foundation
import file_selector_macos
import flutter_secure_storage_macos
import package_info_plus
import path_provider_foundation
import sentry_flutter
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 967 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -81,8 +81,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
crypto:
cross_file:
dependency: transitive
description:
name: cross_file
sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
url: "https://pub.dev"
source: hosted
version: "0.3.3+8"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
@@ -121,6 +129,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03
url: "https://pub.dev"
source: hosted
version: "8.0.0+1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
url: "https://pub.dev"
source: hosted
version: "0.9.2+1"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6
url: "https://pub.dev"
source: hosted
version: "0.9.3+3"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0
url: "https://pub.dev"
source: hosted
version: "0.9.3+1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@@ -158,6 +214,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.22"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
url: "https://pub.dev"
source: hosted
version: "2.0.17"
flutter_secure_storage:
dependency: "direct main"
description:
@@ -256,6 +320,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.7"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1"
url: "https://pub.dev"
source: hosted
version: "0.8.9+3"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3
url: "https://pub.dev"
source: hosted
version: "3.0.2"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "917a5cadd67d052554cfb258595e54217de53fac5b52939426e26319a02e6297"
url: "https://pub.dev"
source: hosted
version: "0.8.9+2"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "3d2c323daea9d60608f1caf30be32a938916f4975434b8352e6f73dae496da38"
url: "https://pub.dev"
source: hosted
version: "2.9.4"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
infinite_scroll_pagination:
dependency: "direct main"
description:
@@ -321,7 +449,7 @@ packages:
source: hosted
version: "1.2.0"
markdown:
dependency: transitive
dependency: "direct main"
description:
name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
@@ -352,6 +480,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
mime:
dependency: transitive
description:
name: mime
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
oauth2:
dependency: "direct main"
description:
@@ -464,6 +600,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.7.4"
sentry:
dependency: transitive
description:
name: sentry
sha256: a460aa48568d47140dd0557410b624d344ffb8c05555107ac65035c1097cf1ad
url: "https://pub.dev"
source: hosted
version: "7.18.0"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: "3d0d1d4e0e407d276ae8128d123263ccbc37e988bae906765efd6f37d544f4c6"
url: "https://pub.dev"
source: hosted
version: "7.18.0"
shared_preferences:
dependency: "direct main"
description:
@@ -541,6 +693,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@@ -653,6 +813,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
uuid:
dependency: transitive
description:
name: uuid
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
url: "https://pub.dev"
source: hosted
version: "4.3.3"
vector_math:
dependency: transitive
description:

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
version: 2.0.0+1
environment:
sdk: '>=3.2.6 <4.0.0'
@@ -46,6 +46,11 @@ dependencies:
flutter_markdown: ^0.6.22
infinite_scroll_pagination: ^4.0.0
flutter_carousel_widget: ^2.2.0
image_picker: ^1.0.7
sentry_flutter: ^7.18.0
crypto: ^3.0.3
file_picker: ^8.0.0+1
markdown: ^7.2.2
dev_dependencies:
flutter_test:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -6,12 +6,18 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <sentry_flutter/sentry_flutter_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
SentryFlutterPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@@ -3,7 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
sentry_flutter
url_launcher_windows
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB