Compare commits

...

17 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
a04bfe4cf9 🚀 Launch 1.3.7+8 2024-10-12 00:56:30 +08:00
7b7988e6cb ♻️ Refactored post layout 2024-10-12 00:41:03 +08:00
67 changed files with 1682 additions and 866 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):
@ -269,7 +273,7 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite (0.0.3): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- "sqlite3 (3.46.1+1)": - "sqlite3 (3.46.1+1)":
@ -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`)
@ -334,7 +339,7 @@ DEPENDENCIES:
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/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:
@ -437,8 +445,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite: sqflite_darwin:
:path: ".symlinks/plugins/sqflite/darwin" :path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs: sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios" :path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios: url_launcher_ios:
@ -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,11 +510,12 @@ 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
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4

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">

View File

@ -83,7 +83,6 @@
</array> </array>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>zh_CN</string>
<string>en</string> <string>en</string>
</array> </array>
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>

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,10 +59,10 @@ 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

@ -192,7 +192,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
const Gap(24), const Gap(24),
Stack( Stack(
children: [ children: [
AccountAvatar(content: _avatar, radius: 40), AttachedCircleAvatar(content: _avatar, radius: 40),
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 40, left: 40,

View File

@ -260,7 +260,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
const Gap(8), const Gap(8),
const Gap(8), const Gap(8),
if (_userinfo != null) if (_userinfo != null)
AccountAvatar(content: _userinfo!.avatar, radius: 16), AttachedCircleAvatar(
content: _userinfo!.avatar, radius: 16),
const Gap(12), const Gap(12),
Expanded( Expanded(
child: Column( child: Column(
@ -587,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';
@ -252,7 +253,7 @@ class _ChatListState extends State<ChatList> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
AccountAvatar( AttachedCircleAvatar(
content: x.avatar, content: x.avatar,
radius: 14, radius: 14,
fallbackWidget: const Icon( fallbackWidget: const Icon(
@ -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,9 +389,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
onUpdate: (_) { onUpdate: (_) {
_pullPosts(); _pullPosts();
}, },
backgroundColor: Theme.of(context) padding: EdgeInsets.symmetric(
.colorScheme vertical: 8,
.surfaceContainerLow, horizontal: 4,
),
), ),
), ),
), ),
@ -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';
@ -13,6 +14,7 @@ import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/navigation/realm_switcher.dart'; import 'package:solian/widgets/navigation/realm_switcher.dart';
import 'package:solian/widgets/posts/post_creation.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_shuffle_swiper.dart'; import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
@ -87,76 +89,89 @@ 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(
height: 48, filter: ImageFilter.blur(
child: const Row( sigmaX: blurSigma,
children: [ sigmaY: blurSigma,
RealmSwitcher(), ),
], child: ListView(
).paddingSymmetric(horizontal: 8), padding: EdgeInsets.zero,
).paddingOnly(top: MediaQuery.of(context).padding.top), physics: const NeverScrollableScrollPhysics(),
children: [
SizedBox(
height: 48,
child: const Row(
children: [
RealmSwitcher(),
],
).paddingSymmetric(horizontal: 8),
).paddingSymmetric(vertical: 4),
TabBar(
controller: _tabController,
dividerHeight: scrollProgress > 0 ? 0 : 0.3,
tabAlignment: TabAlignment.fill,
tabs: [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.feed, size: 20),
const Gap(8),
Text('postListNews'.tr),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.people, size: 20),
const Gap(8),
Text('postListFriends'.tr),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.shuffle_on_outlined,
size: 20,
),
const Gap(8),
Text('postListShuffle'.tr),
],
),
),
],
),
],
).paddingOnly(top: MediaQuery.of(context).padding.top),
),
),
expandedHeight: 104,
snap: true, snap: true,
floating: true, floating: true,
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context), leading: AppBarLeadingButton.adaptive(context),
actions: [ actions: [
const BackgroundStateWidget(), const BackgroundStateWidget(),
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
AppRouter.instance.pushNamed('postSearch');
},
),
const NotificationButton(), const NotificationButton(),
SizedBox( SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16, width: AppTheme.isLargeScreen(context) ? 8 : 16,
), ),
], ],
bottom: TabBar(
controller: _tabController,
dividerHeight: 0.3,
tabAlignment: TabAlignment.fill,
tabs: [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.feed, size: 20),
const Gap(8),
Text('postListNews'.tr),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.people, size: 20),
const Gap(8),
Text('postListFriends'.tr),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.shuffle_on_outlined, size: 20),
const Gap(8),
Text('postListShuffle'.tr),
],
),
),
],
),
); );
}, },
) )
@ -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(),
), ),
@ -225,106 +249,3 @@ class _ExploreScreenState extends State<ExploreScreen>
super.dispose(); super.dispose();
} }
} }
class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox;
const PostCreatePopup({
super.key,
this.hideDraftBox = false,
});
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) {
return const SizedBox.shrink();
}
final List<dynamic> actionList = [
(
icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 0.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 1.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed('draftBox'),
);
},
),
];
return SizedBox(
height: MediaQuery.of(context).size.height * 0.38,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'postNew'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
children: actionList
.map((x) => Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
onTap: x.onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
x.icon,
const Gap(8),
Expanded(
child: Text(
x.label,
overflow: TextOverflow.fade,
),
),
],
).paddingAll(18),
),
))
.toList(),
).paddingSymmetric(horizontal: 20),
),
],
),
);
}
}

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,73 +24,91 @@ 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) {
item = widget.post;
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
return 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(), return const Center(
builder: (context, snapshot) { child: CircularProgressIndicator(),
if (!snapshot.hasData || snapshot.data == null) { );
return const Center( }
child: CircularProgressIndicator(),
);
}
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( if (_isBusy)
child: PostItem( SliverToBoxAdapter(
item: item!, child: LoadingIndicator(),
isClickable: false, ),
isOverrideEmbedClickable: true, SliverToBoxAdapter(
isFullDate: true, child: PostItem(
isFullContent: true, item: _item!,
isShowReply: false, isClickable: false,
isContentSelectable: true, isOverrideEmbedClickable: true,
), isFullDate: true,
), isShowReply: false,
SliverToBoxAdapter( isContentSelectable: true,
child: padding: AppTheme.isLargeScreen(context)
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4), ? EdgeInsets.symmetric(
), horizontal: 4,
SliverToBoxAdapter( vertical: 8,
child: Align( )
alignment: Alignment.centerLeft, : EdgeInsets.zero,
child: Text( ),
'postReplies'.tr, ),
style: Theme.of(context).textTheme.headlineSmall, SliverToBoxAdapter(
).paddingOnly(left: 24, right: 24, top: 16), child: const Divider(thickness: 0.3, height: 1).paddingOnly(
), top: 8,
), ),
PostReplyList(item: item!), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: SizedBox(height: MediaQuery.of(context).padding.bottom), child: Align(
), alignment: Alignment.centerLeft,
], child: Text(
); 'postReplies'.tr,
}, style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 16),
),
),
PostReplyList(
item: _item!,
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
)
: EdgeInsets.zero,
),
SliverToBoxAdapter(
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

@ -156,7 +156,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
size: 18, size: 18,
), ),
) )
: AccountAvatar( : AttachedCircleAvatar(
content: element.avatar!, content: element.avatar!,
bgColor: Theme.of(context).colorScheme.primary, bgColor: Theme.of(context).colorScheme.primary,
), ),

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/auto_cache_image.dart'; import 'package:solian/widgets/auto_cache_image.dart';
class AccountAvatar extends StatelessWidget { class AttachedCircleAvatar extends StatelessWidget {
final dynamic content; final dynamic content;
final Color? bgColor; final Color? bgColor;
final Color? feColor; final Color? feColor;
final double? radius; final double? radius;
final Widget? fallbackWidget; final Widget? fallbackWidget;
const AccountAvatar({ const AttachedCircleAvatar({
super.key, super.key,
required this.content, required this.content,
this.bgColor, this.bgColor,
@ -39,7 +40,7 @@ class AccountAvatar extends StatelessWidget {
child: isEmpty child: isEmpty
? (fallbackWidget ?? ? (fallbackWidget ??
Icon( Icon(
Icons.account_circle, Icons.image,
size: radius != null ? radius! * 1.2 : 24, size: radius != null ? radius! * 1.2 : 24,
color: feColor, color: feColor,
)) ))
@ -48,6 +49,54 @@ class AccountAvatar extends StatelessWidget {
} }
} }
class AccountAvatar extends StatelessWidget {
final dynamic content;
final String username;
final Color? bgColor;
final Color? feColor;
final double? radius;
final Widget? fallbackWidget;
const AccountAvatar({
super.key,
required this.content,
required this.username,
this.bgColor,
this.feColor,
this.radius,
this.fallbackWidget,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
child: AttachedCircleAvatar(
content: content,
bgColor: bgColor,
feColor: feColor,
radius: radius,
fallbackWidget: (fallbackWidget ??
Icon(
Icons.account_circle,
size: radius != null ? radius! * 1.2 : 24,
color: feColor,
)),
),
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: username,
),
);
},
);
}
}
class AccountProfileImage extends StatelessWidget { class AccountProfileImage extends StatelessWidget {
final dynamic content; final dynamic content;
final BoxFit fit; final BoxFit fit;

View File

@ -84,7 +84,7 @@ class AccountHeadingWidget extends StatelessWidget {
Positioned( Positioned(
bottom: -30, bottom: -30,
left: 32, left: 32,
child: AccountAvatar(content: avatar, radius: 40), child: AttachedCircleAvatar(content: avatar, radius: 40),
), ),
], ],
), ),

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

@ -138,7 +138,7 @@ class _AccountSelectorState extends State<AccountSelector> {
return ListTile( return ListTile(
title: Text(element.nick), title: Text(element.nick),
subtitle: Text(element.name), subtitle: Text(element.name),
leading: AccountAvatar(content: element.avatar), leading: AttachedCircleAvatar(content: element.avatar),
trailing: widget.trailingBuilder != null trailing: widget.trailingBuilder != null
? widget.trailingBuilder!(element) ? widget.trailingBuilder!(element)
: _checkSelected(element) : _checkSelected(element)

View File

@ -23,7 +23,7 @@ class SilverRelativeList extends StatelessWidget {
title: Text(element.related.nick), title: Text(element.related.nick),
subtitle: Text(element.related.name), subtitle: Text(element.related.name),
leading: GestureDetector( leading: GestureDetector(
child: AccountAvatar(content: element.related.avatar), child: AttachedCircleAvatar(content: element.related.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -56,7 +56,7 @@ class _RelativeSelectorState extends State<RelativeSelector> {
return ListTile( return ListTile(
title: Text(element.nick), title: Text(element.nick),
subtitle: Text(element.name), subtitle: Text(element.name),
leading: AccountAvatar(content: element.avatar), leading: AttachedCircleAvatar(content: element.avatar),
trailing: widget.trailingBuilder != null trailing: widget.trailingBuilder != null
? widget.trailingBuilder!(element) ? widget.trailingBuilder!(element)
: null, : null,

View File

@ -175,7 +175,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
Row( Row(
children: [ children: [
IgnorePointer( IgnorePointer(
child: AccountAvatar( child: AttachedCircleAvatar(
content: widget.item.account!.avatar, content: widget.item.account!.avatar,
radius: 19, radius: 19,
), ),

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';
@ -19,11 +18,11 @@ class AttachmentList extends StatefulWidget {
final List<Attachment>? attachments; final List<Attachment>? attachments;
final bool isGrid; final bool isGrid;
final bool isColumn; final bool isColumn;
final bool isForceGrid; final bool isFullWidth;
final bool autoload; final bool autoload;
final double flatMaxHeight;
final double columnMaxWidth; final double columnMaxWidth;
final EdgeInsets? padding;
final double? width; final double? width;
final double? viewport; final double? viewport;
@ -34,10 +33,10 @@ class AttachmentList extends StatefulWidget {
this.attachments, this.attachments,
this.isGrid = false, this.isGrid = false,
this.isColumn = false, this.isColumn = false,
this.isForceGrid = false, this.isFullWidth = false,
this.autoload = false, this.autoload = false,
this.flatMaxHeight = 720,
this.columnMaxWidth = 480, this.columnMaxWidth = 480,
this.padding,
this.width, this.width,
this.viewport, this.viewport,
}); });
@ -135,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) {
_getMetadataList(); 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();
}
} else { } else {
setState(() { setState(() {
_attachments = widget.attachments!; _attachments = widget.attachments!;
@ -163,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),
) )
], ],
@ -175,9 +182,79 @@ class _AttachmentListState extends State<AttachmentList> {
.fadeIn(duration: 1250.ms); .fadeIn(duration: 1250.ms);
} }
const radius = BorderRadius.all(Radius.circular(8));
if (widget.isFullWidth && _attachments.length == 1) {
final element = _attachments.first;
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
double ratio =
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
return Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints(
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: _buildEntry(element, 0),
),
),
);
}
final isNotPureImage = _attachments.any(
(x) => x?.mimetype.split('/').firstOrNull != 'image',
);
if (widget.isGrid && !isNotPureImage) {
return GridView.builder(
padding: EdgeInsets.zero,
primary: false,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: math.min(3, _attachments.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
itemCount: _attachments.length,
itemBuilder: (context, idx) {
final element = _attachments[idx];
return Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
);
},
);
}
if (widget.isColumn) { if (widget.isColumn) {
var idx = 0; var idx = 0;
const radius = BorderRadius.all(Radius.circular(8));
return Wrap( return Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
@ -185,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,
@ -195,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,
@ -212,69 +295,52 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final isNotPureImage = _attachments.any( return Container(
(x) => x?.mimetype.split('/').firstOrNull != 'image', constraints: BoxConstraints(
); maxHeight: 320,
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { ),
const radius = BorderRadius.all(Radius.circular(8)); child: ListView.separated(
return GridView.builder( padding: widget.padding,
padding: EdgeInsets.zero, scrollDirection: Axis.horizontal,
primary: false,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: math.min(3, _attachments.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
itemCount: _attachments.length, itemCount: _attachments.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final element = _attachments[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( return Container(
decoration: BoxDecoration( constraints: BoxConstraints(
color: Theme.of(context).colorScheme.surfaceContainerHigh, maxWidth: math.min(
border: Border.all( widget.columnMaxWidth,
color: Theme.of(context).dividerColor, MediaQuery.of(context).size.width -
width: 1, (widget.padding?.horizontal ?? 0),
), ),
borderRadius: radius,
), ),
child: ClipRRect( child: AspectRatio(
borderRadius: radius, aspectRatio: ratio,
child: _buildEntry(element, idx), child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
),
), ),
); );
}, },
).paddingSymmetric(horizontal: 24); separatorBuilder: (context, _) => const Gap(8),
}
return Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints(
maxHeight: widget.flatMaxHeight,
),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.symmetric(
horizontal: BorderSide(
width: 0.3,
color: Theme.of(context).dividerColor,
),
),
),
child: CarouselSlider.builder(
options: CarouselOptions(
animateToClosest: true,
aspectRatio: _aspectRatio,
viewportFraction:
widget.viewport ?? (_attachments.length > 1 ? 0.95 : 1),
enableInfiniteScroll: false,
),
itemCount: _attachments.length,
itemBuilder: (context, idx, _) {
final element = _attachments[idx];
return _buildEntry(element, idx);
},
), ),
); );
} }

View File

@ -205,7 +205,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull; item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
if (item.type == 1 && otherside != null) { if (item.type == 1 && otherside != null) {
final avatar = AccountAvatar( final avatar = AttachedCircleAvatar(
content: otherside.account.avatar, content: otherside.account.avatar,
radius: 20, radius: 20,
bgColor: Theme.of(context).colorScheme.primary, bgColor: Theme.of(context).colorScheme.primary,
@ -241,7 +241,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
elevation: 8, elevation: 8,
), ),
badgeContent: AccountAvatar( badgeContent: AttachedCircleAvatar(
content: item.realm?.avatar, content: item.realm?.avatar,
radius: 10, radius: 10,
fallbackWidget: const Icon( fallbackWidget: const Icon(

View File

@ -152,7 +152,8 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
title: Text(element.account.nick), title: Text(element.account.nick),
subtitle: Text(element.account.name), subtitle: Text(element.account.name),
leading: GestureDetector( leading: GestureDetector(
child: AccountAvatar(content: element.account.avatar), child:
AttachedCircleAvatar(content: element.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -74,7 +74,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
), ),
) )
], ],
child: AccountAvatar( child: AttachedCircleAvatar(
content: widget.userinfo!.avatar, content: widget.userinfo!.avatar,
bgColor: Colors.transparent, bgColor: Colors.transparent,
radius: radius, radius: radius,

View File

@ -220,7 +220,7 @@ class ChatEvent extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
AccountAvatar( AttachedCircleAvatar(
content: item.sender.account.avatar, content: item.sender.account.avatar,
radius: 9, radius: 9,
), ),
@ -250,7 +250,8 @@ class ChatEvent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
child: AccountAvatar(content: item.sender.account.avatar), child:
AttachedCircleAvatar(content: item.sender.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

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

@ -443,7 +443,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
.map( .map(
(x) => ChatMessageSuggestion( (x) => ChatMessageSuggestion(
type: 'users', type: 'users',
leading: AccountAvatar(content: x.avatar), leading: AttachedCircleAvatar(content: x.avatar),
display: x.nick, display: x.nick,
content: '@${x.name}', content: '@${x.name}',
), ),

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();
if (matches.isEmpty) return;
List<LinkMeta> out = List.empty(growable: true);
for (final x in matches) {
final result = await expandController.expandLink(x.group(0)!);
if (result != null) out.add(result);
}
setState(() => _meta = out);
}
@override
void initState() {
super.initState();
_doExpand();
}
@override
Widget build(BuildContext context) {
if (_meta?.isEmpty ?? true) return const SizedBox.shrink();
return Wrap( return Wrap(
children: matches.map((x) { children: _meta!.map((x) {
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: matches.length == 1 ? 480 : 340, maxWidth: _meta!.length == 1 ? 480 : 340,
), ),
child: FutureBuilder( child: Builder(
future: expandController.expandLink(x.group(0)!), builder: (context) {
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
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

@ -69,7 +69,7 @@ class _AppAccountWidgetState extends State<AppAccountWidget> {
bottom: 0, bottom: 0,
end: -2, end: -2,
), ),
child: AccountAvatar( child: AttachedCircleAvatar(
radius: 14, radius: 14,
content: auth.userProfile.value!['avatar'], content: auth.userProfile.value!['avatar'],
), ),

View File

@ -36,7 +36,7 @@ class RealmSwitcher extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (item != null) if (item != null)
AccountAvatar( AttachedCircleAvatar(
content: item.avatar, content: item.avatar,
radius: 14, radius: 14,
fallbackWidget: const Icon( fallbackWidget: const Icon(

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

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox;
const PostCreatePopup({
super.key,
this.hideDraftBox = false,
});
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) {
return const SizedBox.shrink();
}
final List<dynamic> actionList = [
(
icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 0.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 1.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed('draftBox'),
);
},
),
];
return SizedBox(
height: MediaQuery.of(context).size.height * 0.38,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'postNew'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
children: actionList
.map((x) => Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
onTap: x.onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
x.icon,
const Gap(8),
Expanded(
child: Text(
x.label,
overflow: TextOverflow.fade,
),
),
],
).paddingAll(18),
),
))
.toList(),
).paddingSymmetric(horizontal: 20),
),
],
),
);
}
}

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,11 +7,11 @@ 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';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/link_expansion.dart'; import 'package:solian/widgets/link_expansion.dart';
import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/markdown_text_content.dart';
@ -31,11 +30,13 @@ 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;
const PostItem({ const PostItem({
super.key, super.key,
@ -47,11 +48,11 @@ 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,
}); });
@override @override
@ -64,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
@ -89,37 +96,24 @@ 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),
Stack( SizedContainer(
children: [ maxWidth: 640,
SizedContainer( child: MarkdownTextContent(
maxWidth: 640, parentId: 'p${item.id}',
maxHeight: widget.isFullContent ? double.infinity : 80, content: item.body['content'],
child: _MeasureSize( isAutoWarp: item.type == 'story',
onChange: (size) { isSelectable: widget.isContentSelectable,
setState(() => _contentHeight = size.height); ),
}, ).paddingOnly(
child: SingleChildScrollView( left: 12,
physics: const NeverScrollableScrollPhysics(), right: 12,
child: MarkdownTextContent( bottom: hasAttachment ? 4 : 0,
parentId: 'p${item.id}',
content: item.body['content'],
isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable,
),
).paddingOnly(
left: 16,
right: 12,
top: 2,
bottom: hasAttachment ? 4 : 0,
),
),
),
],
), ),
if (_contentHeight >= 80 && !widget.isFullContent) if (widget.item.body?['content_truncated'] == true)
Opacity( Opacity(
opacity: 0.8, opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)), child: InkWell(child: Text('readMore'.tr)),
@ -130,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: 16),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
Row( Row(
children: [ children: [
@ -148,127 +140,85 @@ class _PostItemState extends State<PostItem> {
style: TextStyle(color: _unFocusColor), style: TextStyle(color: _unFocusColor),
) )
], ],
).paddingOnly(left: 16, top: 4), ).paddingOnly(left: 14, top: 4),
], ],
); );
} }
return OpenContainer( return GestureDetector(
tappable: widget.isClickable, child: Column(
closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_PostThumbnail( _PostThumbnail(
rid: item.body['thumbnail'], rid: item.body['thumbnail'],
parentId: widget.item.id.toString(), parentId: widget.item.id.toString(),
).paddingOnly(bottom: 4), ).paddingOnly(bottom: 4),
Row( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( _PostHeaderWidget(
child: AccountAvatar(content: item.author.avatar), isCompact: widget.isCompact,
onTap: () { isFullDate: widget.isFullDate,
showModalBottomSheet( item: item,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: item.author.name,
),
);
},
), ),
Expanded( _PostHeaderDividerWidget(item: item),
child: Column( SizedContainer(
crossAxisAlignment: CrossAxisAlignment.start, maxWidth: 640,
children: [ child: MarkdownTextContent(
_PostHeaderWidget( parentId: 'p${item.id}-embed',
isCompact: widget.isCompact, content: item.body['content'],
item: item, isAutoWarp: item.type == 'story',
), isSelectable: widget.isContentSelectable,
_PostHeaderDividerWidget(item: item),
Stack(
children: [
SizedContainer(
maxWidth: 640,
maxHeight:
widget.isFullContent ? double.infinity : 320,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: MarkdownTextContent(
parentId: 'p${item.id}-embed',
content: item.body['content'],
isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable,
isLargeText: item.type == 'article' &&
widget.isFullContent,
).paddingOnly(left: 12, right: 8),
),
),
),
],
),
if (_contentHeight >= 320 && !widget.isFullContent)
Opacity(
opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)),
).paddingOnly(
left: 12,
top: 4,
),
if (widget.item.replyTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable:
widget.isOverrideEmbedClickable,
item: widget.item.replyTo!,
username: widget.item.replyTo!.author.name,
hintText: 'postRepliedNotify',
icon: FontAwesomeIcons.reply,
id: widget.item.replyTo!.id.toString(),
),
),
if (widget.item.repostTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable:
widget.isOverrideEmbedClickable,
item: widget.item.repostTo!,
username: widget.item.repostTo!.author.name,
hintText: 'postRepostedNotify',
icon: FontAwesomeIcons.retweet,
id: widget.item.repostTo!.id.toString(),
),
),
_PostFooterWidget(item: item).paddingOnly(left: 12),
LinkExpansion(content: item.body['content'])
.paddingOnly(top: 4),
],
), ),
), ),
if (widget.item.body?['content_truncated'] == true)
Opacity(
opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)),
).paddingOnly(top: 4),
if (widget.item.replyTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 8),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
item: widget.item.replyTo!,
username: widget.item.replyTo!.author.name,
hintText: 'postRepliedNotify',
icon: FontAwesomeIcons.reply,
id: widget.item.replyTo!.id.toString(),
),
),
if (widget.item.repostTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 8),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
item: widget.item.repostTo!,
username: widget.item.repostTo!.author.name,
hintText: 'postRepostedNotify',
icon: FontAwesomeIcons.retweet,
id: widget.item.repostTo!.id.toString(),
),
),
_PostFooterWidget(item: item),
LinkExpansion(content: item.body['content']),
], ],
).paddingOnly( ).paddingSymmetric(
top: 10, horizontal: (widget.padding?.horizontal ?? 0) + 16,
bottom:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 0,
right: 16,
left: 16,
), ),
_PostAttachmentWidget(item: item), if (hasAttachment) const Gap(8),
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item), _PostAttachmentWidget(
item: item,
padding: widget.padding,
),
if (widget.showFeaturedReply)
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
horizontal: (widget.padding?.horizontal ?? 0) + 12,
),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,
@ -280,32 +230,30 @@ class _PostItemState extends State<PostItem> {
(item.metric!.reactionList[symbol] ?? 0) + changes; (item.metric!.reactionList[symbol] ?? 0) + changes;
}); });
}, },
onComment: () {
if (widget.onComment != null) {
widget.onComment!();
}
},
).paddingOnly( ).paddingOnly(
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context)) top: 8,
? 10 left: (widget.padding?.left ?? 0) + 14,
: 6, right: (widget.padding?.right ?? 0) + 14,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
bottom: 10,
) )
else
const Gap(10),
], ],
).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,
); );
} }
} }
@ -317,7 +265,6 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLargeScreen = AppTheme.isLargeScreen(context);
final unFocusColor = final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@ -325,19 +272,17 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
return FutureBuilder( return FutureBuilder(
future: future: Get.find<PostProvider>().listPostFeaturedReply(
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()), item.id.toString(),
),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) { if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
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,
@ -351,7 +296,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AccountAvatar( AttachedCircleAvatar(
content: reply.author.avatar, content: reply.author.avatar,
radius: 10, radius: 10,
), ),
@ -423,16 +368,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
.toList(), .toList(),
), ),
), ),
) ).animate().fadeIn(
.animate()
.fadeIn(
duration: 300.ms, duration: 300.ms,
curve: Curves.easeIn, curve: Curves.easeIn,
)
.paddingOnly(
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
right: 16,
); );
}, },
); );
@ -441,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) {
@ -452,29 +391,43 @@ class _PostAttachmentWidget extends StatelessWidget {
? List.from(item.body['attachments']?.whereType<String>()) ? List.from(item.body['attachments']?.whereType<String>())
: List.empty(); : List.empty();
if (attachments.length > 3) { if (attachments.isEmpty) return const SizedBox.shrink();
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,
isGrid: true, isFullWidth: true,
).paddingOnly(left: 36, top: 4, bottom: 4); );
} else if (attachments.length > 1 || isLargeScreen) { } else if (attachments.length == 1) {
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,
isColumn: true, isColumn: true,
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4); ).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
} else { } else if (attachments.length > 1 &&
attachments.length % 3 == 0 &&
!isLargeScreen) {
return AttachmentList( return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
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,
isGrid: true,
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
} else {
return AttachmentList(
parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
padding: EdgeInsets.symmetric(
horizontal: (padding?.horizontal ?? 0) + 14,
),
autoload: false,
); );
} }
} }
@ -515,16 +468,17 @@ class _PostEmbedWidget extends StatelessWidget {
size: 16, size: 16,
color: unFocusColor, color: unFocusColor,
), ),
const Gap(6),
Expanded( Expanded(
child: Text( child: Text(
hintText.trParams( hintText.trParams(
{'username': '@$username'}, {'username': '@$username'},
), ),
style: TextStyle(color: unFocusColor), style: TextStyle(color: unFocusColor),
).paddingOnly(left: 6), ),
), ),
], ],
).paddingOnly(left: 12), ).paddingOnly(left: 2),
Card( Card(
elevation: 1, elevation: 1,
child: PostItem( child: PostItem(
@ -560,9 +514,7 @@ class _PostHeaderDividerWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (item.body['description'] != null || item.body['title'] != null) { if (item.body['description'] != null || item.body['title'] != null) {
return const Divider(thickness: 0.3, height: 1).paddingSymmetric( return const Gap(8);
vertical: 8,
);
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -618,64 +570,94 @@ 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,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (isCompact) Row(
AccountAvatar( crossAxisAlignment: CrossAxisAlignment.start,
content: item.author.avatar, children: [
radius: 10, AccountAvatar(
).paddingOnly(left: 2, top: 1), content: item.author.avatar,
Expanded( username: item.author.name,
child: Column( radius: isCompact ? 10 : null,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ Gap(isCompact ? 6 : 8),
Row( Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
item.author.nick, crossAxisAlignment: CrossAxisAlignment.center,
style: const TextStyle(fontWeight: FontWeight.bold), children: [
Text(
item.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold),
),
if (isCompact) const Gap(4),
if (isCompact)
RelativeDate(
item.publishedAt?.toLocal() ?? DateTime.now(),
isFull: isFullDate,
).paddingOnly(top: 1),
],
), ),
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()) if (!isCompact)
.paddingOnly(left: 4), 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),
),
],
),
], ],
), ),
if (item.body['title'] != null) ),
Text( if (item.type == 'article')
item.body['title'], Badge(
style: Theme.of(context) label: Text('article'.tr),
.textTheme ).paddingOnly(top: 3),
.bodyMedium! ],
.copyWith(fontSize: 15),
),
if (item.body['description'] != null)
Text(
item.body['description'],
style: Theme.of(context).textTheme.bodySmall,
),
],
).paddingOnly(left: isCompact ? 6 : 12),
), ),
if (item.type == 'article') const Gap(8),
Badge( if (item.body['title'] != null)
label: Text('article'.tr), Text(
).paddingOnly(top: 3), item.body['title'],
style: Theme.of(context).textTheme.titleMedium,
),
if (item.body['description'] != null)
Text(
item.body['description'],
style: Theme.of(context).textTheme.titleSmall,
),
], ],
); );
} }
@ -706,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

@ -3,6 +3,8 @@ import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/widgets/posts/post_action.dart'; import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
@ -12,6 +14,7 @@ class PostListWidget extends StatelessWidget {
final bool isNestedClickable; final bool isNestedClickable;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final Color? backgroundColor; final Color? backgroundColor;
final EdgeInsets? padding;
const PostListWidget({ const PostListWidget({
super.key, super.key,
@ -20,6 +23,7 @@ class PostListWidget extends StatelessWidget {
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.backgroundColor, this.backgroundColor,
this.padding,
}); });
@override @override
@ -29,16 +33,18 @@ class PostListWidget extends StatelessWidget {
pagingController: controller, pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>( builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return PostListEntryWidget( return Padding(
isShowEmbed: isShowEmbed, padding: padding ?? EdgeInsets.zero,
isNestedClickable: isNestedClickable, child: PostListEntryWidget(
isClickable: isClickable, isShowEmbed: isShowEmbed,
showFeaturedReply: true, isNestedClickable: isNestedClickable,
item: item, isClickable: isClickable,
backgroundColor: backgroundColor, showFeaturedReply: true,
onUpdate: () { item: item,
controller.refresh(); onUpdate: () {
}, controller.refresh();
},
),
); );
}, },
), ),
@ -53,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,
@ -63,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
@ -76,7 +82,23 @@ class PostListEntryWidget extends StatelessWidget {
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isClickable: isNestedClickable, isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply, showFeaturedReply: showFeaturedReply,
backgroundColor: backgroundColor, padding: padding,
onComment: () {
AppRouter.instance
.pushNamed(
'postEditor',
extra: PostPublishArguments(reply: item),
)
.then((value) {
if (value is Future) {
value.then((_) {
onUpdate();
});
} else if (value != null) {
onUpdate();
}
});
},
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
onLongPress: () { onLongPress: () {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -106,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({
@ -115,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,
}); });
@ -133,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

@ -11,6 +11,7 @@ class PostQuickAction extends StatefulWidget {
final Post item; final Post item;
final bool isReactable; final bool isReactable;
final bool isShowReply; final bool isShowReply;
final Function onComment;
final void Function(String symbol, int num) onReact; final void Function(String symbol, int num) onReact;
const PostQuickAction({ const PostQuickAction({
@ -18,6 +19,7 @@ class PostQuickAction extends StatefulWidget {
required this.item, required this.item,
this.isShowReply = true, this.isShowReply = true,
this.isReactable = true, this.isReactable = true,
required this.onComment,
required this.onReact, required this.onReact,
}); });
@ -106,7 +108,11 @@ class _PostQuickActionState extends State<PostQuickAction> {
builder: (context) { builder: (context) {
return PostReplyListPopup(item: widget.item); return PostReplyListPopup(item: widget.item);
}, },
); ).then((signal) {
if (signal == true) {
widget.onComment();
}
});
}, },
), ),
), ),

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,6 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PostListWidget( return PostListWidget(
padding: widget.padding,
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
@ -70,16 +73,31 @@ class PostReplyListPopup extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
'postReplies'.tr, children: [
style: Theme.of(context).textTheme.headlineSmall, Expanded(
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), child: Text(
'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall,
),
),
IconButton(
icon: const Icon(Icons.add_comment),
visualDensity: const VisualDensity(horizontal: -4),
onPressed: () {
Navigator.pop(context, true);
},
),
],
).paddingOnly(left: 24, right: 24, top: 24, bottom: 8),
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
PostReplyList( PostReplyList(
item: item, item: item,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, padding: EdgeInsets.symmetric(horizontal: 10),
backgroundColor:
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

@ -149,7 +149,8 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
title: Text(element.account.nick), title: Text(element.account.nick),
subtitle: Text(element.account.name), subtitle: Text(element.account.name),
leading: GestureDetector( leading: GestureDetector(
child: AccountAvatar(content: element.account.avatar), child:
AttachedCircleAvatar(content: element.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

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
@ -29,7 +30,7 @@ import protocol_handler_macos
import screen_brightness_macos import screen_brightness_macos
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite import sqflite_darwin
import sqlite3_flutter_libs import sqlite3_flutter_libs
import url_launcher_macos import url_launcher_macos
import wakelock_plus import wakelock_plus
@ -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):
@ -195,7 +199,7 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite (0.0.3): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- "sqlite3 (3.46.1+1)": - "sqlite3 (3.46.1+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`)
@ -249,7 +254,7 @@ DEPENDENCIES:
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -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:
@ -328,8 +336,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation: shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite: sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
sqlite3_flutter_libs: sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
url_launcher_macos: url_launcher_macos:
@ -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
@ -371,16 +380,17 @@ SPEC CHECKSUMS:
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c package_info_plus: d2f71247aab4b6521434f887276093acc70d214c
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
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: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf share_plus: a182a58e04e51647c0481aadabbc4de44b3a2bce
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404

View File

@ -49,7 +49,6 @@
<string>NSApplication</string> <string>NSApplication</string>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>zh_CN</string>
<string>en</string> <string>en</string>
</array> </array>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>

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:
@ -450,10 +442,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.1"
file_picker: file_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -695,10 +687,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_card_swiper name: flutter_card_swiper
sha256: "880ad669017154d6d1f8c3abd861db08af97b3b7b0f7d7d5cbde690a9253811d" sha256: "1eacbfab31b572223042e03409726553aec431abe48af48c8d591d376d070d3d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.2"
flutter_keyboard_visibility: flutter_keyboard_visibility:
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
@ -1401,10 +1401,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.2" version: "8.0.3"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1449,10 +1449,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.11" version: "2.2.12"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@ -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:
@ -1769,18 +1793,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.2" version: "10.0.3"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: share_plus_platform_interface name: share_plus_platform_interface
sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1902,18 +1926,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite name: sqflite
sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.3+2" version: "2.4.0"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.4+4" version: "2.5.4+5"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027"
url: "https://pub.dev"
source: hosted
version: "2.4.1-1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqlite3: sqlite3:
dependency: transitive dependency: transitive
description: description:
@ -2010,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:
@ -2062,10 +2118,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.0" version: "6.3.1"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
@ -2254,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+7 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