Compare commits

..

15 Commits

Author SHA1 Message Date
65c6083640 🚀 Launch 1.3.7+10 2024-10-13 23:19:03 +08:00
ad7a34ec18 Share via image 2024-10-13 23:12:23 +08:00
6c32d76f78 Audit logs 2024-10-13 22:17:23 +08:00
2aa699547c 🐛 Bug fixing on searching 2024-10-13 21:50:47 +08:00
1f4aa8916d Search posts 2024-10-13 21:48:53 +08:00
e2c2e41f89 Improve post detail page first time loading 2024-10-13 21:31:15 +08:00
0f2b854e45 👔 Update level requirements 2024-10-13 20:54:26 +08:00
c21ca5573c Show post visibility 2024-10-13 20:45:00 +08:00
1809f2557d 👽 Support server-side truncate content 2024-10-13 20:36:10 +08:00
1fc84099fe ♻️ Better push token uuid 2024-10-13 20:03:36 +08:00
f8755f5220 🐛 Bug fixes 2024-10-13 19:56:37 +08:00
4041d6dc4e Optimize chat list and attachment loading 2024-10-13 16:26:46 +08:00
cc1071d86e 🚀 Launch 1.3.7+9 2024-10-13 14:58:47 +08:00
e334b862df Auth preferences 2024-10-13 14:13:16 +08:00
32c33a963a 💄 Optimized post list 2024-10-13 01:31:59 +08:00
47 changed files with 1199 additions and 502 deletions

View File

@ -477,5 +477,12 @@
"agedTheme": "Old school style theme", "agedTheme": "Old school style theme",
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.", "agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
"appBackgroundImage": "Global background image", "appBackgroundImage": "Global background image",
"appBackgroundImageDesc": "The global background image will be displayed on all pages" "appBackgroundImageDesc": "The global background image will be displayed on all pages",
"authPreferences": "Auth preferences",
"authPreferencesDesc": "Set the security behavior of your account",
"authMaximumAuthSteps": "Maximum authentication steps",
"authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2",
"auditLog": "Audit log",
"shareImage": "Share as image",
"shareImageFooter": "See more interesting posts on Solar Network"
} }

View File

@ -473,5 +473,12 @@
"agedTheme": "过时主题", "agedTheme": "过时主题",
"agedThemeDesc": "将全局主题降级为 Material Design 2可能发生意料之外的问题仅供实验使用", "agedThemeDesc": "将全局主题降级为 Material Design 2可能发生意料之外的问题仅供实验使用",
"appBackgroundImage": "全局背景图片", "appBackgroundImage": "全局背景图片",
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示" "appBackgroundImageDesc": "全局背景图片将会在所有页面中展示",
"authPreferences": "安全偏好设置",
"authPreferencesDesc": "调整账号的安全行为模式",
"authMaximumAuthSteps": "最大认证步数",
"authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2",
"auditLog": "活动日志",
"shareImage": "分享图片",
"shareImageFooter": "上 Solar Network 看更多有趣帖子"
} }

View File

@ -166,6 +166,9 @@ PODS:
- Flutter - Flutter
- flutter_secure_storage (6.0.0): - flutter_secure_storage (6.0.0):
- Flutter - Flutter
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_webrtc (0.11.3): - flutter_webrtc (0.11.3):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
@ -259,6 +262,7 @@ PODS:
- PromisesObjC (= 2.4.0) - PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1): - protocol_handler_ios (0.0.1):
- Flutter - Flutter
- SAMKeychain (1.5.3)
- screen_brightness_ios (0.1.0): - screen_brightness_ios (0.1.0):
- Flutter - Flutter
- SDWebImage (5.19.7): - SDWebImage (5.19.7):
@ -316,6 +320,7 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`) - gal (from `.symlinks/plugins/gal/darwin`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`)
@ -364,6 +369,7 @@ SPEC REPOS:
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- PromisesSwift - PromisesSwift
- SAMKeychain
- SDWebImage - SDWebImage
- sqlite3 - sqlite3
- SwiftyGif - SwiftyGif
@ -401,6 +407,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage: flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc: flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios" :path: ".symlinks/plugins/flutter_webrtc/ios"
gal: gal:
@ -480,6 +488,7 @@ SPEC CHECKSUMS:
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
@ -501,6 +510,7 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990 protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad

View File

@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
showGraphicsOverview = "Yes"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">

38
lib/models/audit_log.dart Normal file
View File

@ -0,0 +1,38 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart';
part 'audit_log.g.dart';
@JsonSerializable()
class AuditEvent {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String type;
String target;
String location;
String ipAddress;
String userAgent;
Account account;
int accountId;
AuditEvent({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.type,
required this.target,
required this.location,
required this.ipAddress,
required this.userAgent,
required this.account,
required this.accountId,
});
static AuditEvent fromJson(Map<String, dynamic> json) =>
_$AuditEventFromJson(json);
Map<String, dynamic> toJson() => _$AuditEventToJson(this);
}

View File

@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audit_log.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AuditEvent _$AuditEventFromJson(Map<String, dynamic> json) => AuditEvent(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
type: json['type'] as String,
target: json['target'] as String,
location: json['location'] as String,
ipAddress: json['ip_address'] as String,
userAgent: json['user_agent'] as String,
account: Account.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$AuditEventToJson(AuditEvent instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'type': instance.type,
'target': instance.target,
'location': instance.location,
'ip_address': instance.ipAddress,
'user_agent': instance.userAgent,
'account': instance.account.toJson(),
'account_id': instance.accountId,
};

View File

@ -24,6 +24,7 @@ class Post {
String? alias; String? alias;
String? areaAlias; String? areaAlias;
dynamic body; dynamic body;
int visibility;
List<Tag>? tags; List<Tag>? tags;
List<Category>? categories; List<Category>? categories;
List<Post>? replies; List<Post>? replies;
@ -55,6 +56,7 @@ class Post {
required this.areaAlias, required this.areaAlias,
required this.type, required this.type,
required this.body, required this.body,
required this.visibility,
required this.tags, required this.tags,
required this.categories, required this.categories,
required this.replies, required this.replies,

View File

@ -20,6 +20,7 @@ Post _$PostFromJson(Map<String, dynamic> json) => Post(
areaAlias: json['area_alias'] as String?, areaAlias: json['area_alias'] as String?,
type: json['type'] as String, type: json['type'] as String,
body: json['body'], body: json['body'],
visibility: (json['visibility'] as num).toInt(),
tags: (json['tags'] as List<dynamic>?) tags: (json['tags'] as List<dynamic>?)
?.map((e) => Tag.fromJson(e as Map<String, dynamic>)) ?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
@ -67,6 +68,7 @@ Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
'alias': instance.alias, 'alias': instance.alias,
'area_alias': instance.areaAlias, 'area_alias': instance.areaAlias,
'body': instance.body, 'body': instance.body,
'visibility': instance.visibility,
'tags': instance.tags?.map((e) => e.toJson()).toList(), 'tags': instance.tags?.map((e) => e.toJson()).toList(),
'categories': instance.categories?.map((e) => e.toJson()).toList(), 'categories': instance.categories?.map((e) => e.toJson()).toList(),
'replies': instance.replies?.map((e) => e.toJson()).toList(), 'replies': instance.replies?.map((e) => e.toJson()).toList(),

View File

@ -23,6 +23,21 @@ class AttachmentProvider extends GetConnect {
final Map<String, Attachment> _cachedResponses = {}; final Map<String, Attachment> _cachedResponses = {};
List<Attachment?> listMetadataFromCache(List<String> rid) {
if (rid.isEmpty) return List.empty();
List<Attachment?> result = List.filled(rid.length, null);
for (var idx = 0; idx < rid.length; idx++) {
if (_cachedResponses.containsKey(rid[idx])) {
result[idx] = _cachedResponses[rid[idx]];
} else {
result[idx] = null;
}
}
return result;
}
Future<List<Attachment?>> listMetadata( Future<List<Attachment?>> listMetadata(
List<String> rid, { List<String> rid, {
noCache = false, noCache = false,

View File

@ -54,6 +54,27 @@ class PostProvider extends GetxController {
return resp; return resp;
} }
Future<Response> searchPost(String probe, int page,
{String? realm, String? author, tag, category, int take = 10}) async {
final queries = [
'probe=$probe',
'take=$take',
'offset=$page',
if (tag != null) 'tag=$tag',
if (category != null) 'category=$category',
if (author != null) 'author=$author',
if (realm != null) 'realm=$realm',
];
final AuthProvider auth = Get.find();
final client = await auth.configureClient('co');
final resp = await client.get('/posts/search?${queries.join('&')}');
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return resp;
}
Future<Response> listPost(int page, Future<Response> listPost(int page,
{String? realm, String? author, tag, category, int take = 10}) async { {String? realm, String? author, tag, category, int take = 10}) async {
final queries = [ final queries = [

View File

@ -4,19 +4,19 @@ import 'package:intl/intl.dart';
class ExperienceProvider extends GetxController { class ExperienceProvider extends GetxController {
static List<int> experienceToLevelRequirements = [ static List<int> experienceToLevelRequirements = [
0, // Level 0 0, // Level 0
100, // Level 1 1000, // Level 1
400, // Level 2 4000, // Level 2
900, // Level 3 9000, // Level 3
1600, // Level 4 16000, // Level 4
2500, // Level 5 25000, // Level 5
3600, // Level 6 36000, // Level 6
4900, // Level 7 49000, // Level 7
6400, // Level 8 64000, // Level 8
8100, // Level 9 81000, // Level 9
10000, // Level 10 100000, // Level 10
12100, // Level 11 121000, // Level 11
14400, // Level 12 144000, // Level 12
36800 // Level 13 368000 // Level 13
]; ];
static List<String> levelLabelMapping = static List<String> levelLabelMapping =
@ -35,7 +35,7 @@ class ExperienceProvider extends GetxController {
final idx = experienceToLevelRequirements.indexOf(exp); final idx = experienceToLevelRequirements.indexOf(exp);
if (idx + 1 >= experienceToLevelRequirements.length) return 1; if (idx + 1 >= experienceToLevelRequirements.length) return 1;
final nextExp = experienceToLevelRequirements[idx + 1]; final nextExp = experienceToLevelRequirements[idx + 1];
return exp / nextExp; return (experience - exp).abs() / (exp - nextExp).abs();
} }
static String calcLevelUpProgressLevel(int experience) { static String calcLevelUpProgressLevel(int experience) {
@ -43,9 +43,9 @@ class ExperienceProvider extends GetxController {
.firstWhere((x) => x <= experience); .firstWhere((x) => x <= experience);
final idx = experienceToLevelRequirements.indexOf(exp); final idx = experienceToLevelRequirements.indexOf(exp);
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity'; if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
final nextExp = experienceToLevelRequirements[idx + 1]; final nextExp = exp - experienceToLevelRequirements[idx + 1];
final formatter = final formatter =
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1); NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
return '${formatter.format(exp)}/${formatter.format(nextExp)}'; return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}';
} }
} }

View File

@ -3,9 +3,9 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/request.dart';
@ -165,10 +165,11 @@ class WebSocketProvider extends GetxController {
late final String? token; late final String? token;
late final String provider; late final String provider;
final deviceUuid = await _getDeviceUuid(); var deviceUuid = await _getDeviceUuid();
if (deviceUuid == null || deviceUuid.isEmpty) { if (deviceUuid == null || deviceUuid.isEmpty) {
log("Unable to active push notifications, couldn't get device uuid"); log("Unable to active push notifications, couldn't get device uuid");
return;
} else { } else {
log('Device UUID is $deviceUuid'); log('Device UUID is $deviceUuid');
} }
@ -195,33 +196,7 @@ class WebSocketProvider extends GetxController {
} }
Future<String?> _getDeviceUuid() async { Future<String?> _getDeviceUuid() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (PlatformInfo.isWeb) return null;
if (PlatformInfo.isWeb) { return await FlutterUdid.consistentUdid;
final webInfo = await deviceInfo.webBrowserInfo;
return webInfo.vendor! +
webInfo.userAgent! +
webInfo.hardwareConcurrency.toString();
}
if (PlatformInfo.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
return androidInfo.id;
}
if (PlatformInfo.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
return iosInfo.identifierForVendor!;
}
if (PlatformInfo.isLinux) {
final linuxInfo = await deviceInfo.linuxInfo;
return linuxInfo.machineId!;
}
if (PlatformInfo.isWindows) {
final windowsInfo = await deviceInfo.windowsInfo;
return windowsInfo.deviceId;
}
if (PlatformInfo.isMacOS) {
final macosInfo = await deviceInfo.macOsInfo;
return macosInfo.systemGUID;
}
return null;
} }
} }

View File

@ -2,11 +2,14 @@ import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/bootstrapper.dart'; import 'package:solian/bootstrapper.dart';
import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart'; import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart'; import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/audit_log.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/preferences/notifications.dart'; import 'package:solian/screens/account/preferences/notifications.dart';
import 'package:solian/screens/account/preferences/security.dart';
import 'package:solian/screens/account/profile_edit.dart'; import 'package:solian/screens/account/profile_edit.dart';
import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
@ -16,9 +19,9 @@ import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/chat.dart'; import 'package:solian/screens/chat.dart';
import 'package:solian/screens/dashboard.dart'; import 'package:solian/screens/dashboard.dart';
import 'package:solian/screens/feed/search.dart'; import 'package:solian/screens/posts/post_search.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/screens/feed/draft_box.dart'; import 'package:solian/screens/posts/draft_box.dart';
import 'package:solian/screens/realms.dart'; import 'package:solian/screens/realms.dart';
import 'package:solian/screens/realms/realm_detail.dart'; import 'package:solian/screens/realms/realm_detail.dart';
import 'package:solian/screens/realms/realm_organize.dart'; import 'package:solian/screens/realms/realm_organize.dart';
@ -94,7 +97,7 @@ abstract class AppRouter {
name: 'postSearch', name: 'postSearch',
builder: (context, state) => TitleShell( builder: (context, state) => TitleShell(
state: state, state: state,
child: FeedSearchScreen( child: PostSearchScreen(
tag: state.uri.queryParameters['tag'], tag: state.uri.queryParameters['tag'],
category: state.uri.queryParameters['category'], category: state.uri.queryParameters['category'],
), ),
@ -107,6 +110,7 @@ abstract class AppRouter {
state: state, state: state,
child: PostDetailScreen( child: PostDetailScreen(
id: state.pathParameters['id']!, id: state.pathParameters['id']!,
post: state.extra as Post?,
), ),
), ),
), ),
@ -264,6 +268,22 @@ abstract class AppRouter {
child: const NotificationPreferencesScreen(), child: const NotificationPreferencesScreen(),
), ),
), ),
GoRoute(
path: '/account/preferences/auth',
name: 'authPreferences',
builder: (context, state) => TitleShell(
state: state,
child: const AuthPreferencesScreen(),
),
),
GoRoute(
path: '/account/audit',
name: 'auditLog',
builder: (context, state) => TitleShell(
state: state,
child: const AuditLogScreen(),
),
),
GoRoute( GoRoute(
path: '/account/view/:name', path: '/account/view/:name',
name: 'accountProfilePage', name: 'accountProfilePage',

View File

@ -129,6 +129,24 @@ class _AccountScreenState extends State<AccountScreen> {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('settings');
}, },
), ),
if (auth.isAuthorized.value)
ListTile(
leading: const Icon(Icons.event_repeat),
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
title: Text('auditLog'.tr),
onTap: () {
AppRouter.instance.pushNamed('auditLog');
},
),
if (auth.isAuthorized.value)
ListTile(
leading: const Icon(Icons.lock),
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
title: Text('authPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('authPreferences');
},
),
if (auth.isAuthorized.value) if (auth.isAuthorized.value)
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34), contentPadding: const EdgeInsets.symmetric(horizontal: 34),

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/audit_log.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/relative_date.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:timeline_tile/timeline_tile.dart';
class AuditLogScreen extends StatefulWidget {
const AuditLogScreen({super.key});
@override
State<AuditLogScreen> createState() => _AuditLogScreenState();
}
class _AuditLogScreenState extends State<AuditLogScreen> {
bool _isBusy = true;
final List<AuditEvent> _events = List.empty(growable: true);
Future<void> _getEvents() async {
if (!_isBusy) setState(() => _isBusy = true);
final AuthProvider auth = Get.find();
final client = await auth.configureClient('id');
final resp =
await client.get('/users/me/events?take=10&offset=${_events.length}');
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
}
final result = PaginationResult.fromJson(resp.body);
setState(() {
_events.addAll(
result.data?.map((x) => AuditEvent.fromJson(x)).toList() ??
List.empty(),
);
_isBusy = false;
});
}
@override
void initState() {
super.initState();
_getEvents();
}
@override
Widget build(BuildContext context) {
return InfiniteList(
itemCount: _events.length,
isLoading: _isBusy,
onFetchData: () {
_getEvents();
},
itemBuilder: (context, idx) {
final element = _events[idx];
return TimelineTile(
isFirst: idx == 0,
isLast: _events.length - 1 == idx,
alignment: TimelineAlign.start,
endChild: Container(
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
element.type,
style: GoogleFonts.robotoMono(fontSize: 15),
),
Row(
children: [
RelativeDate(element.createdAt),
const Gap(6),
Text('·'),
const Gap(6),
RelativeDate(element.createdAt, isFull: true),
],
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
).paddingOnly(left: 16),
),
).paddingSymmetric(horizontal: 18);
},
);
}
}

View File

@ -59,9 +59,9 @@ class _NotificationPreferencesScreenState
}); });
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp)); context.showErrorDialog(RequestException(resp));
} } else {
context.showSnackbar('preferencesApplied'.tr); context.showSnackbar('preferencesApplied'.tr);
}
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/auth.dart';
class AuthPreferencesScreen extends StatefulWidget {
const AuthPreferencesScreen({super.key});
@override
State<AuthPreferencesScreen> createState() => _AuthPreferencesScreenState();
}
class _AuthPreferencesScreenState extends State<AuthPreferencesScreen> {
bool _isBusy = true;
Map<String, dynamic> _config = {
'maximum_auth_steps': 2,
};
Future<void> _getPreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.get('/preferences/auth');
if (resp.statusCode != 200 && resp.statusCode != 404) {
context.showErrorDialog(RequestException(resp));
}
if (resp.statusCode == 200) {
_config = resp.body;
}
setState(() => _isBusy = false);
}
Future<void> _savePreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.put('/preferences/auth', _config);
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
} else {
context.showSnackbar('preferencesApplied'.tr);
}
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
_getPreferences();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainer,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.save),
title: Text('save'.tr),
enabled: !_isBusy,
onTap: () {
_savePreferences();
},
),
Expanded(
child: ListView(
children: [
ListTile(
title: Text('authMaximumAuthSteps'.tr),
subtitle: Text('authMaximumAuthStepsDesc'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: SizedBox(
width: 60,
child: _isBusy
? null
: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
isDense: true,
),
initialValue:
_config['maximum_auth_steps']?.toString() ?? '2',
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onChanged: (value) {
_config['maximum_auth_steps'] =
int.tryParse(value) ?? 2;
},
),
),
),
],
),
),
],
);
}
}

View File

@ -588,8 +588,6 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
color: color:
Theme.of(context).colorScheme.surfaceContainerLow, Theme.of(context).colorScheme.surfaceContainerLow,
child: PostListEntryWidget( child: PostListEntryWidget(
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
item: element, item: element,
isClickable: true, isClickable: true,
isNestedClickable: true, isNestedClickable: true,

View File

@ -19,6 +19,7 @@ import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/channel/channel_list.dart';
import 'package:solian/widgets/chat/call/chat_call_indicator.dart'; import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sidebar/empty_placeholder.dart'; import 'package:solian/widgets/sidebar/empty_placeholder.dart';
@ -280,26 +281,7 @@ class _ChatListState extends State<ChatList> {
return Column( return Column(
children: [ children: [
const ChatCallCurrentIndicator(), const ChatCallCurrentIndicator(),
if (_isBusy) if (_isBusy) const LoadingIndicator(),
Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0.8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(strokeWidth: 2.5),
),
const Gap(8),
Text('loading'.tr)
],
).paddingSymmetric(vertical: 8),
),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
children: [ children: [

View File

@ -389,10 +389,11 @@ class _DashboardScreenState extends State<DashboardScreen> {
onUpdate: (_) { onUpdate: (_) {
_pullPosts(); _pullPosts();
}, },
backgroundColor: Theme.of(context) padding: EdgeInsets.symmetric(
.colorScheme vertical: 8,
.surfaceContainerLow, horizontal: 4,
).paddingAll(8), ),
),
), ),
), ),
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),
@ -525,7 +526,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
style: TextStyle(color: _unFocusColor, fontSize: 12), style: TextStyle(color: _unFocusColor, fontSize: 12),
) )
], ],
).paddingAll(8), ).paddingOnly(left: 8, right: 8, top: 8, bottom: 50),
], ],
), ),
); );

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -6,6 +7,7 @@ import 'package:get/get.dart';
import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart';
@ -87,42 +89,30 @@ class _ExploreScreenState extends State<ExploreScreen>
final scrollProgress = final scrollProgress =
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0); (scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
final backgroundColor = Color.lerp( final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0;
Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0),
Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0.9),
scrollProgress,
);
return SliverAppBar( return SliverAppBar(
backgroundColor: backgroundColor, flexibleSpace: ClipRRect(
flexibleSpace: SizedBox( child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: blurSigma,
sigmaY: blurSigma,
),
child: ListView(
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
children: [
SizedBox(
height: 48, height: 48,
child: const Row( child: const Row(
children: [ children: [
RealmSwitcher(), RealmSwitcher(),
], ],
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),
).paddingOnly(top: MediaQuery.of(context).padding.top), ).paddingSymmetric(vertical: 4),
snap: true, TabBar(
floating: true,
toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context),
actions: [
const BackgroundStateWidget(),
const NotificationButton(),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar(
controller: _tabController, controller: _tabController,
dividerHeight: 0.3, dividerHeight: scrollProgress > 0 ? 0 : 0.3,
tabAlignment: TabAlignment.fill, tabAlignment: TabAlignment.fill,
tabs: [ tabs: [
Tab( Tab(
@ -149,7 +139,10 @@ class _ExploreScreenState extends State<ExploreScreen>
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.shuffle_on_outlined, size: 20), const Icon(
Icons.shuffle_on_outlined,
size: 20,
),
const Gap(8), const Gap(8),
Text('postListShuffle'.tr), Text('postListShuffle'.tr),
], ],
@ -157,6 +150,28 @@ class _ExploreScreenState extends State<ExploreScreen>
), ),
], ],
), ),
],
).paddingOnly(top: MediaQuery.of(context).padding.top),
),
),
expandedHeight: 104,
snap: true,
floating: true,
toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context),
actions: [
const BackgroundStateWidget(),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
AppRouter.instance.pushNamed('postSearch');
},
),
const NotificationButton(),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
); );
}, },
) )
@ -180,6 +195,12 @@ class _ExploreScreenState extends State<ExploreScreen>
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
ControlledPostListWidget( ControlledPostListWidget(
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
)
: EdgeInsets.zero,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),
@ -191,6 +212,9 @@ class _ExploreScreenState extends State<ExploreScreen>
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
ControlledPostListWidget( ControlledPostListWidget(
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(horizontal: 16)
: EdgeInsets.zero,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),

View File

@ -1,99 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/posts/post_list.dart';
import '../../models/post.dart';
class FeedSearchScreen extends StatefulWidget {
final String? tag;
final String? category;
const FeedSearchScreen({super.key, this.tag, this.category});
@override
State<FeedSearchScreen> createState() => _FeedSearchScreenState();
}
class _FeedSearchScreenState extends State<FeedSearchScreen> {
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
getPosts(int pageKey) async {
final PostProvider provider = Get.find();
Response resp;
try {
resp = await provider.listPost(
pageKey,
tag: widget.tag,
category: widget.category,
);
} catch (e) {
_pagingController.error = e;
return;
}
final PaginationResult result = PaginationResult.fromJson(resp.body);
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
}
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(getPosts);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (widget.tag != null)
ListTile(
leading: const Icon(Icons.label),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
),
if (widget.category != null)
ListTile(
leading: const Icon(Icons.category),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('postSearchWithCategory'
.trParams({'key': widget.category!})),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
ControlledPostListWidget(
controller: _pagingController,
onUpdate: () => _pagingController.refresh(),
),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}

View File

@ -4,6 +4,8 @@ import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart'; import 'package:solian/providers/last_read.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart'; import 'package:solian/widgets/posts/post_replies.dart';
@ -22,35 +24,37 @@ class PostDetailScreen extends StatefulWidget {
} }
class _PostDetailScreenState extends State<PostDetailScreen> { class _PostDetailScreenState extends State<PostDetailScreen> {
Post? item; bool _isBusy = true;
Future<Post?> _getDetail() async { Post? _item;
if (widget.post != null) {
setState(() {
item = widget.post;
});
}
final PostProvider provider = Get.find(); Future<void> _getDetail() async {
final PostProvider posts = Get.find();
try { try {
final resp = await provider.getPost(widget.id); final resp = await posts.getPost(widget.id);
item = Post.fromJson(resp.body); _item = Post.fromJson(resp.body);
} catch (e) { } catch (e) {
context.showErrorDialog(e).then((_) => Navigator.pop(context)); context.showErrorDialog(e).then((_) => Navigator.pop(context));
} }
Get.find<LastReadProvider>().feedLastReadAt = item?.id; Get.find<LastReadProvider>().feedLastReadAt = _item?.id;
return item; setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
if (widget.post != null) {
_item = widget.post;
}
_getDetail();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( if (_isBusy && _item == null) {
future: _getDetail(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
@ -58,20 +62,30 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: LoadingIndicator(),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: PostItem( child: PostItem(
item: item!, item: _item!,
isClickable: false, isClickable: false,
isOverrideEmbedClickable: true, isOverrideEmbedClickable: true,
isFullDate: true, isFullDate: true,
isFullContent: true,
isShowReply: false, isShowReply: false,
isContentSelectable: true, isContentSelectable: true,
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
)
: EdgeInsets.zero,
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: child: const Divider(thickness: 0.3, height: 1).paddingOnly(
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4), top: 8,
),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Align( child: Align(
@ -82,13 +96,19 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
).paddingOnly(left: 24, right: 24, top: 16), ).paddingOnly(left: 24, right: 24, top: 16),
), ),
), ),
PostReplyList(item: item!), PostReplyList(
item: _item!,
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
)
: EdgeInsets.zero,
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: SizedBox(height: MediaQuery.of(context).padding.bottom), child: SizedBox(height: MediaQuery.of(context).padding.bottom),
), ),
], ],
); );
},
);
} }
} }

View File

@ -182,7 +182,10 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow, tileColor: Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0.5),
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_list.dart';
import '../../models/post.dart';
class PostSearchScreen extends StatefulWidget {
final String? tag;
final String? category;
const PostSearchScreen({super.key, this.tag, this.category});
@override
State<PostSearchScreen> createState() => _PostSearchScreenState();
}
class _PostSearchScreenState extends State<PostSearchScreen> {
final TextEditingController _probeController = TextEditingController();
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
late bool _isBusy = widget.tag != null || widget.category != null;
_searchPosts(int pageKey) async {
if (widget.tag == null &&
widget.category == null &&
_probeController.text.isEmpty) {
_pagingController.appendLastPage([]);
return;
}
if (!_isBusy) {
setState(() => _isBusy = true);
}
if (pageKey == 0) {
_pagingController.itemList?.clear();
_pagingController.nextPageKey = 0;
}
final PostProvider provider = Get.find();
Response resp;
try {
if (_probeController.text.isEmpty) {
resp = await provider.listPost(
pageKey,
tag: widget.tag,
category: widget.category,
);
} else {
resp = await provider.searchPost(
_probeController.text,
pageKey,
tag: widget.tag,
category: widget.category,
);
}
} catch (e) {
_pagingController.error = e;
return;
}
final PaginationResult result = PaginationResult.fromJson(resp.body);
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(_searchPosts);
}
@override
void dispose() {
_probeController.dispose();
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
if (widget.tag != null)
ListTile(
leading: const Icon(Icons.label),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
tileColor: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
),
if (widget.category != null)
ListTile(
leading: const Icon(Icons.category),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
tileColor: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
title: Text('postSearchWithCategory'.trParams({
'key': widget.category!,
})),
),
Container(
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.5),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: TextField(
controller: _probeController,
decoration: InputDecoration(
isCollapsed: true,
border: InputBorder.none,
hintText: 'search'.tr,
),
onSubmitted: (_) {
_searchPosts(0);
},
),
),
if (_isBusy) const LoadingIndicator(),
Expanded(
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
ControlledPostListWidget(
controller: _pagingController,
onUpdate: () => _pagingController.refresh(),
),
SliverGap(MediaQuery.of(context).padding.bottom),
],
),
),
),
],
),
);
}
}

View File

@ -89,8 +89,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.75, height: MediaQuery.of(context).size.height * 0.75,
child: Column( child: ListView(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AccountHeadingWidget( AccountHeadingWidget(
avatar: _userinfo!.avatar, avatar: _userinfo!.avatar,

View File

@ -155,11 +155,18 @@ class _AttachmentItemImage extends StatelessWidget {
), ),
if (showBadge && badge != null) if (showBadge && badge != null)
Positioned( Positioned(
right: 12, right: 8,
bottom: 8, bottom: 4,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Chip(label: Text(badge!)), child: Chip(
label: Text(badge!),
labelStyle: GoogleFonts.robotoMono(),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
),
), ),
), ),
if (showHideButton && item.isMature) if (showHideButton && item.isMature)

View File

@ -1,7 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart' hide CarouselController; import 'package:flutter/material.dart' hide CarouselController;
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@ -23,6 +22,7 @@ class AttachmentList extends StatefulWidget {
final bool autoload; final bool autoload;
final double columnMaxWidth; final double columnMaxWidth;
final EdgeInsets? padding;
final double? width; final double? width;
final double? viewport; final double? viewport;
@ -36,6 +36,7 @@ class AttachmentList extends StatefulWidget {
this.isFullWidth = false, this.isFullWidth = false,
this.autoload = false, this.autoload = false,
this.columnMaxWidth = 480, this.columnMaxWidth = 480,
this.padding,
this.width, this.width,
this.viewport, this.viewport,
}); });
@ -133,7 +134,17 @@ class _AttachmentListState extends State<AttachmentList> {
super.initState(); super.initState();
assert(widget.attachmentIds != null || widget.attachments != null); assert(widget.attachmentIds != null || widget.attachments != null);
if (widget.attachments == null) { if (widget.attachments == null) {
final AttachmentProvider attach = Get.find();
final cachedResult = attach.listMetadataFromCache(widget.attachmentIds!);
if (cachedResult.every((x) => x != null)) {
setState(() {
_attachments = cachedResult;
_isLoading = false;
});
_calculateAspectRatio();
} else {
_getMetadataList(); _getMetadataList();
}
} else { } else {
setState(() { setState(() {
_attachments = widget.attachments!; _attachments = widget.attachments!;
@ -161,9 +172,7 @@ class _AttachmentListState extends State<AttachmentList> {
color: _unFocusColor, color: _unFocusColor,
).paddingOnly(right: 5), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams( 'attachmentHint'.trParams({'count': _attachments.toString()}),
{'count': _attachments.toString()},
),
style: TextStyle(color: _unFocusColor, fontSize: 12), style: TextStyle(color: _unFocusColor, fontSize: 12),
) )
], ],
@ -177,16 +186,22 @@ class _AttachmentListState extends State<AttachmentList> {
if (widget.isFullWidth && _attachments.length == 1) { if (widget.isFullWidth && _attachments.length == 1) {
final element = _attachments.first; final element = _attachments.first;
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9; final isImage = element!.mimetype.split('/').firstOrNull == 'image';
double ratio =
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
return Container( return Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640, maxHeight: 640,
), ),
child: AspectRatio( child: AspectRatio(
aspectRatio: ratio, aspectRatio: ratio,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.symmetric( border: Border.symmetric(
horizontal: BorderSide( horizontal: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
@ -219,7 +234,10 @@ class _AttachmentListState extends State<AttachmentList> {
final element = _attachments[idx]; final element = _attachments[idx];
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@ -244,7 +262,9 @@ class _AttachmentListState extends State<AttachmentList> {
final element = _attachments[idx]; final element = _attachments[idx];
idx++; idx++;
if (element == null) return const SizedBox.shrink(); if (element == null) return const SizedBox.shrink();
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9; final isImage = element.mimetype.split('/').firstOrNull == 'image';
double ratio =
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth, maxWidth: widget.columnMaxWidth,
@ -254,6 +274,10 @@ class _AttachmentListState extends State<AttachmentList> {
aspectRatio: ratio, aspectRatio: ratio,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@ -271,31 +295,37 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
return SizedBox(
width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth),
child: CarouselSlider.builder(
options: CarouselOptions(
disableCenter: true,
animateToClosest: true,
aspectRatio: _aspectRatio,
enlargeCenterPage: true,
viewportFraction: widget.viewport ?? 0.95,
enableInfiniteScroll: false,
),
itemCount: _attachments.length,
itemBuilder: (context, idx, _) {
final element = _attachments[idx];
if (element == null) const SizedBox.shrink();
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth, maxHeight: 320,
maxHeight: 640, ),
child: ListView.separated(
padding: widget.padding,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: _attachments.length,
itemBuilder: (context, idx) {
final element = _attachments[idx];
if (element == null) const SizedBox.shrink();
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
double ratio =
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
return Container(
constraints: BoxConstraints(
maxWidth: math.min(
widget.columnMaxWidth,
MediaQuery.of(context).size.width -
(widget.padding?.horizontal ?? 0),
),
), ),
child: AspectRatio( child: AspectRatio(
aspectRatio: ratio, aspectRatio: ratio,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@ -310,6 +340,7 @@ class _AttachmentListState extends State<AttachmentList> {
), ),
); );
}, },
separatorBuilder: (context, _) => const Gap(8),
), ),
); );
} }

View File

@ -37,6 +37,7 @@ class ChatEventList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomScrollView( return CustomScrollView(
cacheExtent: 100,
reverse: true, reverse: true,
slivers: [ slivers: [
Obx(() { Obx(() {

View File

@ -2,15 +2,21 @@ import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/link.dart';
import 'package:solian/providers/link_expander.dart'; import 'package:solian/providers/link_expander.dart';
import 'package:solian/widgets/auto_cache_image.dart'; import 'package:solian/widgets/auto_cache_image.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class LinkExpansion extends StatelessWidget { class LinkExpansion extends StatefulWidget {
final String content; final String content;
const LinkExpansion({super.key, required this.content}); const LinkExpansion({super.key, required this.content});
@override
State<LinkExpansion> createState() => _LinkExpansionState();
}
class _LinkExpansionState extends State<LinkExpansion> {
Widget _buildImage(String url, {double? width, double? height}) { Widget _buildImage(String url, {double? width, double? height}) {
if (url.endsWith('svg')) { if (url.endsWith('svg')) {
return SvgPicture.network(url, width: width, height: height); return SvgPicture.network(url, width: width, height: height);
@ -22,61 +28,74 @@ class LinkExpansion extends StatelessWidget {
); );
} }
@override List<LinkMeta>? _meta;
Widget build(BuildContext context) {
Future<void> _doExpand() async {
final linkRegex = RegExp( final linkRegex = RegExp(
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]', r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
); );
final matches = linkRegex.allMatches(content); final matches = linkRegex.allMatches(widget.content);
if (matches.isEmpty) { if (matches.isEmpty) return;
return const SizedBox.shrink();
}
final LinkExpandProvider expandController = Get.find(); final LinkExpandProvider expandController = Get.find();
return Wrap( if (matches.isEmpty) return;
children: matches.map((x) {
return Container( List<LinkMeta> out = List.empty(growable: true);
constraints: BoxConstraints( for (final x in matches) {
maxWidth: matches.length == 1 ? 480 : 340, final result = await expandController.expandLink(x.group(0)!);
), if (result != null) out.add(result);
child: FutureBuilder(
future: expandController.expandLink(x.group(0)!),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
} }
setState(() => _meta = out);
}
@override
void initState() {
super.initState();
_doExpand();
}
@override
Widget build(BuildContext context) {
if (_meta?.isEmpty ?? true) return const SizedBox.shrink();
return Wrap(
children: _meta!.map((x) {
return Container(
constraints: BoxConstraints(
maxWidth: _meta!.length == 1 ? 480 : 340,
),
child: Builder(
builder: (context) {
final isRichDescription = [ final isRichDescription = [
'solsynth.dev', 'solsynth.dev',
].contains(Uri.parse(snapshot.data!.url).host); ].contains(Uri.parse(x.url).host);
return GestureDetector( return GestureDetector(
child: Card( child: Card(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if ([ if ([(x.icon?.isNotEmpty ?? false), x.siteName != null]
(snapshot.data!.icon?.isNotEmpty ?? false), .any((x) => x))
snapshot.data!.siteName != null
].any((x) => x))
Row( Row(
children: [ children: [
if (snapshot.data!.icon?.isNotEmpty ?? false) if (x.icon?.isNotEmpty ?? false)
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
child: _buildImage( child: _buildImage(
snapshot.data!.icon!, x.icon!,
width: 32, width: 32,
height: 32, height: 32,
), ),
).paddingOnly(right: 8), ).paddingOnly(right: 8),
if (snapshot.data!.siteName != null) if (x.siteName != null)
Expanded( Expanded(
child: Text( child: Text(
snapshot.data!.siteName!, x.siteName!,
style: Theme.of(context).textTheme.labelLarge, style: Theme.of(context).textTheme.labelLarge,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -84,32 +103,27 @@ class LinkExpansion extends StatelessWidget {
), ),
], ],
).paddingOnly( ).paddingOnly(
bottom: (snapshot.data!.icon?.isNotEmpty ?? false) bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4,
? 8
: 4,
), ),
if (snapshot.data!.image != null && if (x.image != null &&
(snapshot.data!.image?.startsWith('http') ?? false)) (x.image?.startsWith('http') ?? false))
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
child: _buildImage( child: _buildImage(x.image!),
snapshot.data!.image!,
),
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
Text( Text(
snapshot.data!.title ?? 'No Title', x.title ?? 'No Title',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
if (snapshot.data!.description != null && if (x.description != null && isRichDescription)
isRichDescription) MarkdownBody(data: x.description!)
MarkdownBody(data: snapshot.data!.description!) else if (x.description != null)
else if (snapshot.data!.description != null)
Text( Text(
snapshot.data!.description!, x.description!,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -117,7 +131,7 @@ class LinkExpansion extends StatelessWidget {
).paddingAll(12), ).paddingAll(12),
), ),
onTap: () { onTap: () {
launchUrlString(x.group(0)!); launchUrlString(x.url);
}, },
); );
}, },

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
class LoadingIndicator extends StatelessWidget {
const LoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(strokeWidth: 2.5),
),
const Gap(8),
Text('loading'.tr)
],
),
);
}
}

View File

@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
@ -5,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
@ -12,6 +15,7 @@ import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/widgets/posts/post_share.dart';
import 'package:solian/widgets/reports/abuse_report.dart'; import 'package:solian/widgets/reports/abuse_report.dart';
class PostAction extends StatefulWidget { class PostAction extends StatefulWidget {
@ -84,6 +88,24 @@ class _PostActionState extends State<PostAction> {
} }
} }
Future<void> _shareImage() async {
final screenshot = ScreenshotController();
final image = await screenshot.captureFromWidget(
PostShareImage(item: widget.item),
context: context,
);
final directory = await getApplicationDocumentsDirectory();
final imageFile = await File(
'${directory.path}/temporary_share_image.png',
).create();
await imageFile.writeAsBytes(image);
final file = XFile(imageFile.path);
await Share.shareXFiles([file]);
await imageFile.delete();
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -135,16 +157,29 @@ class _PostActionState extends State<PostAction> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.share), leading: const Icon(Icons.share),
title: Text('share'.tr), title: Text('share'.tr),
trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid trailing: Row(
? IconButton( mainAxisSize: MainAxisSize.min,
children: [
if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
IconButton(
icon: const Icon(Icons.link_off), icon: const Icon(Icons.link_off),
tooltip: 'shareNoUri'.tr, tooltip: 'shareNoUri'.tr,
onPressed: () async { onPressed: () async {
await _doShare(noUri: true); await _doShare(noUri: true);
Navigator.pop(context); Navigator.pop(context);
}, },
) ),
: null, if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
IconButton(
icon: const Icon(Icons.image),
tooltip: 'shareImage'.tr,
onPressed: () async {
await _shareImage();
Navigator.pop(context);
},
),
],
),
onTap: () async { onTap: () async {
await _doShare(); await _doShare();
Navigator.pop(context); Navigator.pop(context);

View File

@ -1,6 +1,5 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -8,6 +7,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart'; import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@ -30,11 +30,12 @@ class PostItem extends StatefulWidget {
final bool isShowEmbed; final bool isShowEmbed;
final bool isOverrideEmbedClickable; final bool isOverrideEmbedClickable;
final bool isFullDate; final bool isFullDate;
final bool isFullContent;
final bool isContentSelectable; final bool isContentSelectable;
final bool showFeaturedReply; final bool showFeaturedReply;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor;
final EdgeInsets? padding;
final Function? onComment; final Function? onComment;
const PostItem({ const PostItem({
@ -47,11 +48,10 @@ class PostItem extends StatefulWidget {
this.isShowEmbed = true, this.isShowEmbed = true,
this.isOverrideEmbedClickable = false, this.isOverrideEmbedClickable = false,
this.isFullDate = false, this.isFullDate = false,
this.isFullContent = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.showFeaturedReply = false, this.showFeaturedReply = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.padding,
this.onComment, this.onComment,
}); });
@ -65,14 +65,20 @@ class _PostItemState extends State<PostItem> {
Color get _unFocusColor => Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
static final visibilityIcons = [
Icons.public,
Icons.group,
Icons.visibility,
Icons.visibility_off,
Icons.lock,
];
@override @override
void initState() { void initState() {
item = widget.item; item = widget.item;
super.initState(); super.initState();
} }
double _contentHeight = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<String> attachments = item.body['attachments'] is List final List<String> attachments = item.body['attachments'] is List
@ -90,18 +96,12 @@ class _PostItemState extends State<PostItem> {
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
_PostHeaderWidget( _PostHeaderWidget(
isCompact: widget.isCompact, isCompact: widget.isCompact,
isFullDate: widget.isFullDate,
item: item, item: item,
).paddingSymmetric(horizontal: 12), ).paddingSymmetric(horizontal: 12),
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12), _PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
SizedContainer( SizedContainer(
maxWidth: 640, maxWidth: 640,
maxHeight: widget.isFullContent ? double.infinity : 80,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: MarkdownTextContent( child: MarkdownTextContent(
parentId: 'p${item.id}', parentId: 'p${item.id}',
content: item.body['content'], content: item.body['content'],
@ -113,9 +113,7 @@ class _PostItemState extends State<PostItem> {
right: 12, right: 12,
bottom: hasAttachment ? 4 : 0, bottom: hasAttachment ? 4 : 0,
), ),
), if (widget.item.body?['content_truncated'] == true)
),
if (_contentHeight >= 80 && !widget.isFullContent)
Opacity( Opacity(
opacity: 0.8, opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)), child: InkWell(child: Text('readMore'.tr)),
@ -126,9 +124,7 @@ class _PostItemState extends State<PostItem> {
LinkExpansion(content: item.body['content']).paddingOnly( LinkExpansion(content: item.body['content']).paddingOnly(
left: 8, left: 8,
right: 8, right: 8,
top: 4,
), ),
_PostFooterWidget(item: item).paddingOnly(left: 12),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
Row( Row(
children: [ children: [
@ -149,9 +145,8 @@ class _PostItemState extends State<PostItem> {
); );
} }
return OpenContainer( return GestureDetector(
tappable: widget.isClickable, child: Column(
closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_PostThumbnail( _PostThumbnail(
@ -163,30 +158,20 @@ class _PostItemState extends State<PostItem> {
children: [ children: [
_PostHeaderWidget( _PostHeaderWidget(
isCompact: widget.isCompact, isCompact: widget.isCompact,
isFullDate: widget.isFullDate,
item: item, item: item,
), ),
_PostHeaderDividerWidget(item: item), _PostHeaderDividerWidget(item: item),
SizedContainer( SizedContainer(
maxWidth: 640, maxWidth: 640,
maxHeight: widget.isFullContent ? double.infinity : 320,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: MarkdownTextContent( child: MarkdownTextContent(
parentId: 'p${item.id}-embed', parentId: 'p${item.id}-embed',
content: item.body['content'], content: item.body['content'],
isAutoWarp: item.type == 'story', isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
isLargeText:
item.type == 'article' && widget.isFullContent,
), ),
), ),
), if (widget.item.body?['content_truncated'] == true)
),
if (_contentHeight >= 320 && !widget.isFullContent)
Opacity( Opacity(
opacity: 0.8, opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)), child: InkWell(child: Text('readMore'.tr)),
@ -220,18 +205,20 @@ class _PostItemState extends State<PostItem> {
), ),
), ),
_PostFooterWidget(item: item), _PostFooterWidget(item: item),
LinkExpansion(content: item.body['content']).paddingOnly(top: 4), LinkExpansion(content: item.body['content']),
], ],
).paddingOnly( ).paddingSymmetric(
right: 16, horizontal: (widget.padding?.horizontal ?? 0) + 16,
left: 16, ),
if (hasAttachment) const Gap(8),
_PostAttachmentWidget(
item: item,
padding: widget.padding,
), ),
_PostAttachmentWidget(item: item),
if (widget.showFeaturedReply) if (widget.showFeaturedReply)
_PostFeaturedReplyWidget(item: item).paddingSymmetric( _PostFeaturedReplyWidget(item: item).paddingSymmetric(
horizontal: 12, horizontal: (widget.padding?.horizontal ?? 0) + 12,
), ),
if (widget.showFeaturedReply) const Gap(8),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,
@ -249,22 +236,24 @@ class _PostItemState extends State<PostItem> {
} }
}, },
).paddingOnly( ).paddingOnly(
left: 14, top: 8,
right: 14, left: (widget.padding?.left ?? 0) + 14,
right: (widget.padding?.right ?? 0) + 14,
) )
], ],
).paddingOnly(
top: widget.padding?.top ?? 0,
bottom: widget.padding?.bottom ?? 0,
), ),
openBuilder: (_, __) => TitleShell( onTap: () {
title: 'postDetail'.tr, if (widget.isClickable) {
child: PostDetailScreen( AppRouter.instance.pushNamed(
id: item.id.toString(), 'postDetail',
post: item, pathParameters: {'id': item.id.toString()},
), extra: item,
), );
closedElevation: 0, }
openElevation: 0, },
closedColor: Colors.transparent,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
} }
@ -293,6 +282,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
} }
return Container( return Container(
padding: EdgeInsets.only(top: 8),
constraints: const BoxConstraints(maxWidth: 480), constraints: const BoxConstraints(maxWidth: 480),
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
@ -389,8 +379,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
class _PostAttachmentWidget extends StatelessWidget { class _PostAttachmentWidget extends StatelessWidget {
final Post item; final Post item;
final EdgeInsets? padding;
const _PostAttachmentWidget({required this.item}); const _PostAttachmentWidget({required this.item, required this.padding});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -402,14 +393,22 @@ class _PostAttachmentWidget extends StatelessWidget {
if (attachments.isEmpty) return const SizedBox.shrink(); if (attachments.isEmpty) return const SizedBox.shrink();
if (attachments.length == 1) { if (attachments.length == 1 && !isLargeScreen) {
return AttachmentList( return AttachmentList(
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null, attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments, attachments: item.preload?.attachments,
autoload: false, autoload: false,
isFullWidth: true, isFullWidth: true,
).paddingOnly(top: 4); );
} else if (attachments.length == 1) {
return AttachmentList(
parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
autoload: false,
isColumn: true,
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
} else if (attachments.length > 1 && } else if (attachments.length > 1 &&
attachments.length % 3 == 0 && attachments.length % 3 == 0 &&
!isLargeScreen) { !isLargeScreen) {
@ -419,14 +418,17 @@ class _PostAttachmentWidget extends StatelessWidget {
attachments: item.preload?.attachments, attachments: item.preload?.attachments,
autoload: false, autoload: false,
isGrid: true, isGrid: true,
).paddingSymmetric(horizontal: 14, vertical: 8); ).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
} else { } else {
return AttachmentList( return AttachmentList(
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null, attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments, attachments: item.preload?.attachments,
padding: EdgeInsets.symmetric(
horizontal: (padding?.horizontal ?? 0) + 14,
),
autoload: false, autoload: false,
).paddingOnly(bottom: 8, top: 4); );
} }
} }
} }
@ -568,17 +570,19 @@ class _PostFooterWidget extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: widgets, children: widgets,
).paddingOnly(top: 4); ).paddingSymmetric(vertical: 4);
} }
} }
} }
class _PostHeaderWidget extends StatelessWidget { class _PostHeaderWidget extends StatelessWidget {
final bool isCompact; final bool isCompact;
final bool isFullDate;
final Post item; final Post item;
const _PostHeaderWidget({ const _PostHeaderWidget({
required this.isCompact, required this.isCompact,
required this.isFullDate,
required this.item, required this.item,
}); });
@ -611,11 +615,29 @@ class _PostHeaderWidget extends StatelessWidget {
if (isCompact) if (isCompact)
RelativeDate( RelativeDate(
item.publishedAt?.toLocal() ?? DateTime.now(), item.publishedAt?.toLocal() ?? DateTime.now(),
isFull: isFullDate,
).paddingOnly(top: 1), ).paddingOnly(top: 1),
], ],
), ),
if (!isCompact) if (!isCompact)
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()), Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
RelativeDate(
item.publishedAt?.toLocal() ?? DateTime.now(),
isFull: isFullDate,
),
const Gap(4),
Icon(
_PostItemState.visibilityIcons[item.visibility],
size: 16,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
),
],
),
], ],
), ),
), ),
@ -666,45 +688,3 @@ class _PostThumbnail extends StatelessWidget {
); );
} }
} }
typedef _OnWidgetSizeChange = void Function(Size size);
class _MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
_OnWidgetSizeChange onChange;
_MeasureSizeRenderObject(this.onChange);
@override
void performLayout() {
super.performLayout();
Size newSize = child!.size;
if (oldSize == newSize) return;
oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) {
onChange(newSize);
});
}
}
class _MeasureSize extends SingleChildRenderObjectWidget {
final _OnWidgetSizeChange onChange;
const _MeasureSize({
required this.onChange,
required Widget super.child,
});
@override
RenderObject createRenderObject(BuildContext context) {
return _MeasureSizeRenderObject(onChange);
}
@override
void updateRenderObject(
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}

View File

@ -41,7 +41,6 @@ class PostListWidget extends StatelessWidget {
isClickable: isClickable, isClickable: isClickable,
showFeaturedReply: true, showFeaturedReply: true,
item: item, item: item,
backgroundColor: backgroundColor,
onUpdate: () { onUpdate: () {
controller.refresh(); controller.refresh();
}, },
@ -60,8 +59,8 @@ class PostListEntryWidget extends StatelessWidget {
final bool isClickable; final bool isClickable;
final bool showFeaturedReply; final bool showFeaturedReply;
final Post item; final Post item;
final EdgeInsets? padding;
final Function onUpdate; final Function onUpdate;
final Color? backgroundColor;
const PostListEntryWidget({ const PostListEntryWidget({
super.key, super.key,
@ -70,8 +69,8 @@ class PostListEntryWidget extends StatelessWidget {
required this.isClickable, required this.isClickable,
required this.showFeaturedReply, required this.showFeaturedReply,
required this.item, required this.item,
this.padding,
required this.onUpdate, required this.onUpdate,
this.backgroundColor,
}); });
@override @override
@ -83,7 +82,7 @@ class PostListEntryWidget extends StatelessWidget {
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isClickable: isNestedClickable, isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply, showFeaturedReply: showFeaturedReply,
backgroundColor: backgroundColor, padding: padding,
onComment: () { onComment: () {
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
@ -129,6 +128,7 @@ class ControlledPostListWidget extends StatelessWidget {
final bool isNestedClickable; final bool isNestedClickable;
final bool isPinned; final bool isPinned;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final EdgeInsets? padding;
final Function? onUpdate; final Function? onUpdate;
const ControlledPostListWidget({ const ControlledPostListWidget({
@ -138,6 +138,7 @@ class ControlledPostListWidget extends StatelessWidget {
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.isPinned = true, this.isPinned = true,
this.padding,
this.onUpdate, this.onUpdate,
}); });
@ -156,6 +157,7 @@ class ControlledPostListWidget extends StatelessWidget {
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
showFeaturedReply: true, showFeaturedReply: true,
padding: padding,
item: item, item: item,
onUpdate: onUpdate ?? () {}, onUpdate: onUpdate ?? () {},
); );

View File

@ -31,8 +31,6 @@ class PostOwnedListEntry extends StatelessWidget {
isClickable: false, isClickable: false,
isShowReply: false, isShowReply: false,
isReactable: false, isReactable: false,
isFullContent: isFullContent,
backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
], ],
), ),

View File

@ -8,11 +8,13 @@ import 'package:solian/widgets/posts/post_list.dart';
class PostReplyList extends StatefulWidget { class PostReplyList extends StatefulWidget {
final Post item; final Post item;
final EdgeInsets? padding;
final Color? backgroundColor; final Color? backgroundColor;
const PostReplyList({ const PostReplyList({
super.key, super.key,
required this.item, required this.item,
this.padding,
this.backgroundColor, this.backgroundColor,
}); });
@ -53,7 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PostListWidget( return PostListWidget(
padding: EdgeInsets.symmetric(horizontal: 10), padding: widget.padding,
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
@ -93,6 +95,7 @@ class PostReplyListPopup extends StatelessWidget {
slivers: [ slivers: [
PostReplyList( PostReplyList(
item: item, item: item,
padding: EdgeInsets.symmetric(horizontal: 10),
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow, Theme.of(context).colorScheme.surfaceContainerLow,
), ),

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/root_container.dart';
class PostShareImage extends StatelessWidget {
final Post item;
const PostShareImage({super.key, required this.item});
@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.3);
return RootContainer(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Gap(24),
Material(
color: Colors.transparent,
child: Card(
child: PostItem(
item: item,
isShowEmbed: true,
isClickable: false,
showFeaturedReply: false,
isReactable: false,
isShowReply: false,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
onComment: () {},
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset(
'assets/logo.png',
width: 48,
height: 48,
),
),
const Gap(16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'shareImageFooter'.tr,
style: TextStyle(
fontSize: 13,
color: textColor,
),
),
Text(
'Solsynth LLC © ${DateTime.now().year}',
style: TextStyle(
fontSize: 11,
color: textColor,
),
),
],
),
],
),
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: QrImageView(
data: 'https://solsynth.dev/posts/${item.id}',
version: QrVersions.auto,
padding: const EdgeInsets.all(4),
size: 48,
),
),
),
],
),
],
).paddingSymmetric(horizontal: 36, vertical: 24),
);
}
}

View File

@ -25,7 +25,6 @@ class PostSingleDisplay extends StatelessWidget {
isNestedClickable: true, isNestedClickable: true,
showFeaturedReply: true, showFeaturedReply: true,
onUpdate: onUpdate, onUpdate: onUpdate,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
), ),
), ),
), ),

View File

@ -10,6 +10,7 @@
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h> #include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_udid/flutter_udid_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
@ -30,6 +31,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar);
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);

View File

@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux file_selector_linux
flutter_acrylic flutter_acrylic
flutter_secure_storage_linux flutter_secure_storage_linux
flutter_udid
flutter_webrtc flutter_webrtc
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video

View File

@ -15,6 +15,7 @@ import firebase_crashlytics
import firebase_messaging import firebase_messaging
import flutter_local_notifications import flutter_local_notifications
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_udid
import flutter_webrtc import flutter_webrtc
import gal import gal
import in_app_review import in_app_review
@ -45,6 +46,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))

View File

@ -101,6 +101,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- flutter_secure_storage_macos (6.1.1): - flutter_secure_storage_macos (6.1.1):
- FlutterMacOS - FlutterMacOS
- flutter_udid (0.0.1):
- FlutterMacOS
- SAMKeychain
- flutter_webrtc (0.11.3): - flutter_webrtc (0.11.3):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
@ -188,6 +191,7 @@ PODS:
- PromisesObjC (= 2.4.0) - PromisesObjC (= 2.4.0)
- protocol_handler_macos (0.0.1): - protocol_handler_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- SAMKeychain (1.5.3)
- screen_brightness_macos (0.1.0): - screen_brightness_macos (0.1.0):
- FlutterMacOS - FlutterMacOS
- share_plus (0.0.1): - share_plus (0.0.1):
@ -233,6 +237,7 @@ DEPENDENCIES:
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
@ -272,6 +277,7 @@ SPEC REPOS:
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- PromisesSwift - PromisesSwift
- SAMKeychain
- sqlite3 - sqlite3
- WebRTC-SDK - WebRTC-SDK
@ -296,6 +302,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
flutter_secure_storage_macos: flutter_secure_storage_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
flutter_udid:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
flutter_webrtc: flutter_webrtc:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
FlutterMacOS: FlutterMacOS:
@ -358,6 +366,7 @@ SPEC CHECKSUMS:
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
flutter_udid: 6b2b89780c3dfeecf0047bdf93f622d6416b1c07
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9 flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
@ -377,6 +386,7 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
share_plus: a182a58e04e51647c0481aadabbc4de44b3a2bce share_plus: a182a58e04e51647c0481aadabbc4de44b3a2bce
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78

View File

@ -198,14 +198,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
carousel_slider:
dependency: "direct main"
description:
name: carousel_slider
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -791,10 +783,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_markdown name: flutter_markdown
sha256: e17575ca576a34b46c58c91f9948891117a1bd97815d2e661813c7f90c647a78 sha256: bd9c475d9aae256369edacafc29d1e74c81f78a10cdcdacbbbc9e3c43d009e4a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3+2" version: "0.7.4"
flutter_native_splash: flutter_native_splash:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -896,6 +888,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.2.0" version: "5.2.0"
flutter_udid:
dependency: "direct main"
description:
name: flutter_udid
sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_web_plugins: flutter_web_plugins:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -1685,6 +1685,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
recase: recase:
dependency: transitive dependency: transitive
description: description:
@ -1757,6 +1773,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3" version: "0.1.3"
screenshot:
dependency: "direct main"
description:
name: screenshot
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sdp_transform: sdp_transform:
dependency: transitive dependency: transitive
description: description:
@ -2034,6 +2058,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.7.0" version: "3.7.0"
timeline_tile:
dependency: "direct main"
description:
name: timeline_tile
sha256: "85ec2023c67137397c2812e3e848b2fb20b410b67cd9aff304bb5480c376fc0c"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:
@ -2278,10 +2310,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.5" version: "5.6.0"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.3.7+8 version: 1.3.7+10
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -18,7 +18,6 @@ dependencies:
flutter_markdown: ^0.7.1 flutter_markdown: ^0.7.1
flutter_animate: ^4.5.0 flutter_animate: ^4.5.0
flutter_secure_storage: ^9.2.1 flutter_secure_storage: ^9.2.1
carousel_slider: ^5.0.0
url_launcher: ^6.2.6 url_launcher: ^6.2.6
infinite_scroll_pagination: ^4.0.0 infinite_scroll_pagination: ^4.0.0
image_picker: ^1.1.1 image_picker: ^1.1.1
@ -84,6 +83,10 @@ dependencies:
action_slider: ^0.7.0 action_slider: ^0.7.0
in_app_review: ^2.0.9 in_app_review: ^2.0.9
syntax_highlight: ^0.4.0 syntax_highlight: ^0.4.0
flutter_udid: ^3.0.0
timeline_tile: ^2.0.0
screenshot: ^3.0.0
qr_flutter: ^4.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -12,6 +12,7 @@
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h> #include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <flutter_udid/flutter_udid_plugin_c_api.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <gal/gal_plugin_c_api.h> #include <gal/gal_plugin_c_api.h>
#include <livekit_client/live_kit_plugin.h> #include <livekit_client/live_kit_plugin.h>
@ -38,6 +39,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
FlutterUdidPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
FlutterWebRTCPluginRegisterWithRegistrar( FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
GalPluginCApiRegisterWithRegistrar( GalPluginCApiRegisterWithRegistrar(

View File

@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
firebase_core firebase_core
flutter_acrylic flutter_acrylic
flutter_secure_storage_windows flutter_secure_storage_windows
flutter_udid
flutter_webrtc flutter_webrtc
gal gal
livekit_client livekit_client