Compare commits

...

10 Commits

Author SHA1 Message Date
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
81a616157e 🚀 Launch v1.3.7+7 2024-10-11 01:10:23 +08:00
52312662fb 🐛 Fix post visibility issue 2024-10-11 00:28:09 +08:00
ca18d6ade4 💄 Chat channels loading indicator 2024-10-10 23:48:37 +08:00
af7cc8dab0 💄 Optimize the explore page app bar behavior 2024-10-10 23:36:07 +08:00
382e3c4a4c Optimize post attachment loading 2024-10-10 22:52:05 +08:00
1e37c6ddae 🚀 Launch 1.3.6+6 (to App Store) 2024-10-08 00:52:13 +08:00
442ef06147 💄 Optimize post editor 2024-10-08 00:06:08 +08:00
606a0d708a 🍱 Add cpp highlight file 2024-10-07 23:35:06 +08:00
46 changed files with 1416 additions and 775 deletions

13
.roadsignrc Normal file
View File

@ -0,0 +1,13 @@
{
"sync": {
"region": "solian",
"configPath": "roadsign.toml"
},
"deployments": [
{
"region": "solian",
"site": "solian-web",
"path": "build/web"
}
]
}

View File

@ -0,0 +1,358 @@
{
"name": "C++",
"version": "1.0.0",
"fileTypes": ["cpp", "hpp", "cc", "h"],
"scopeName": "source.cpp",
"foldingStartMarker": "\\{\\s*$",
"foldingStopMarker": "^\\s*\\}",
"patterns": [
{
"name": "meta.preprocessor.script.cpp",
"match": "^\\s*#\\s*(include|define|if|ifdef|ifndef|else|endif|pragma)\\b"
},
{
"name": "meta.declaration.cpp",
"begin": "^\\w*\\b(namespace|class|struct|enum|typedef|template)\\b",
"beginCaptures": {
"0": {
"name": "keyword.other.declaration.cpp"
}
},
"end": "(\\{|;)",
"endCaptures": {
"0": {
"name": "punctuation.terminator.cpp"
}
},
"patterns": [
{
"include": "#strings"
},
{
"include": "#comments"
},
{
"name": "keyword.other.cpp",
"match": "\\b(public|private|protected|virtual|override|final)\\b"
}
]
},
{
"include": "#comments"
},
{
"include": "#punctuation"
},
{
"include": "#annotations"
},
{
"include": "#keywords"
},
{
"include": "#constants-and-special-vars"
},
{
"include": "#operators"
},
{
"include": "#strings"
}
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.block.empty.cpp",
"match": "/\\*\\*/",
"captures": {
"0": {
"name": "punctuation.definition.comment.cpp"
}
}
},
{
"include": "#comments-doc-oldschool"
},
{
"include": "#comments-doc"
},
{
"include": "#comments-inline"
}
]
},
"comments-doc-oldschool": {
"patterns": [
{
"name": "comment.block.documentation.cpp",
"begin": "/\\*\\*",
"end": "\\*/",
"patterns": [
{
"include": "#comments-doc-oldschool"
},
{
"include": "#comments-block"
}
]
}
]
},
"comments-doc": {
"patterns": [
{
"name": "comment.block.documentation.cpp",
"begin": "///",
"while": "^\\s*///",
"patterns": [
{
"include": "#comments-inline"
}
]
}
]
},
"comments-inline": {
"patterns": [
{
"include": "#comments-block"
},
{
"match": "(//.*)$",
"captures": {
"1": {
"name": "comment.line.double-slash.cpp"
}
}
}
]
},
"comments-block": {
"patterns": [
{
"name": "comment.block.cpp",
"begin": "/\\*",
"end": "\\*/",
"patterns": [
{
"include": "#comments-block"
}
]
}
]
},
"annotations": {
"patterns": [
{
"name": "storage.type.annotation.cpp",
"match": "__attribute__\\(\\w+\\)"
}
]
},
"constants-and-special-vars": {
"patterns": [
{
"name": "constant.language.cpp",
"match": "\\b(true|false|nullptr)\\b"
},
{
"name": "variable.language.cpp",
"match": "\\b(this|super)\\b"
},
{
"name": "constant.numeric.cpp",
"match": "\\b((0(x|X)[0-9a-fA-F]+)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)\\b"
},
{
"include": "#class-identifier"
},
{
"include": "#function-identifier"
}
]
},
"class-identifier": {
"patterns": [
{
"match": "\\b(bool|int|char|double|float|long|short|signed|unsigned|void)\\b",
"name": "storage.type.primitive.cpp"
},
{
"begin": "(\\b[A-Z]\\w*\\b)",
"end": "(?!<)",
"beginCaptures": {
"1": {
"name": "support.class.cpp"
}
},
"patterns": [
{
"include": "#type-args"
}
]
}
]
},
"function-identifier": {
"patterns": [
{
"match": "\\b([a-z_][a-zA-Z0-9_]*)\\s*\\(",
"captures": {
"1": {
"name": "entity.name.function.cpp"
}
}
}
]
},
"type-args": {
"begin": "(<)",
"end": "(>)",
"beginCaptures": {
"1": {
"name": "other.source.cpp"
}
},
"endCaptures": {
"1": {
"name": "other.source.cpp"
}
},
"patterns": [
{
"include": "#class-identifier"
},
{
"match": ","
},
{
"name": "keyword.declaration.cpp",
"match": "extends"
},
{
"include": "#comments"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.cpp",
"match": "\\b(if|else|for|while|do|switch|case|break|continue|goto|return)\\b"
},
{
"name": "keyword.operator.cpp",
"match": "\\b(sizeof|typeid|decltype|new|delete)\\b"
},
{
"name": "keyword.control.try.cpp",
"match": "\\b(try|catch|throw)\\b"
},
{
"name": "keyword.control.cpp",
"match": "\\b(static|inline|virtual|override|const|volatile|explicit|friend|constexpr)\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.comparison.cpp",
"match": "(==|!=|<=?|>=?)"
},
{
"name": "keyword.operator.arithmetic.cpp",
"match": "(\\+|\\-|\\*|\\/|%)"
},
{
"name": "keyword.operator.assignment.cpp",
"match": "(=|\\+=|-=|\\*=|/=|%=)"
},
{
"name": "keyword.operator.logical.cpp",
"match": "(\\&\\&|\\|\\||!)"
},
{
"name": "keyword.operator.bitwise.cpp",
"match": "(<<|>>|\\&|\\||\\^|~)"
}
]
},
"string-interp": {
"patterns": [
{
"match": "\\$([a-zA-Z0-9_]+)",
"captures": {
"1": {
"name": "variable.parameter.cpp"
}
}
},
{
"name": "string.interpolated.expression.cpp",
"begin": "\\$\\{",
"end": "\\}",
"patterns": [
{
"include": "#constants-and-special-vars",
"name": "variable.parameter.cpp"
},
{
"include": "#strings"
},
{
"name": "variable.parameter.cpp",
"match": "[a-zA-Z0-9_]+"
}
]
},
{
"name": "constant.character.escape.cpp",
"match": "\\\\."
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.cpp",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.cpp",
"match": "\\\\."
}
]
},
{
"name": "string.quoted.single.cpp",
"begin": "'",
"end": "'",
"patterns": [
{
"name": "constant.character.escape.cpp",
"match": "\\\\."
}
]
}
]
},
"punctuation": {
"patterns": [
{
"name": "punctuation.comma.cpp",
"match": ","
},
{
"name": "punctuation.terminator.cpp",
"match": ";"
}
]
}
}
}

View File

@ -269,7 +269,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)":
@ -334,7 +334,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`)
@ -437,8 +437,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:
@ -505,7 +505,7 @@ SPEC CHECKSUMS:
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

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

View File

@ -1,9 +1,12 @@
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:get/get.dart'; 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/attachment.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/attachment.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';
@ -31,9 +34,18 @@ class PostListController extends GetxController {
pagingController.addPageRequestListener(_onPagingControllerRequest); pagingController.addPageRequestListener(_onPagingControllerRequest);
} }
Completer<void>? _pagingLoadCompleter;
Future<void> _onPagingControllerRequest(int pageKey) async { Future<void> _onPagingControllerRequest(int pageKey) async {
try { try {
if (_pagingLoadCompleter != null) {
await _pagingLoadCompleter!.future;
return;
}
_pagingLoadCompleter = Completer();
final result = await loadMore(); final result = await loadMore();
_pagingLoadCompleter!.complete();
_pagingLoadCompleter = null;
if (result != null && hasMore.value) { if (result != null && hasMore.value) {
pagingController.appendPage(result, nextPageKey.value); pagingController.appendPage(result, nextPageKey.value);
@ -97,9 +109,6 @@ class PostListController extends GetxController {
hasMore.value = false; hasMore.value = false;
} }
final idx = <dynamic>{};
postList.retainWhere((x) => idx.add(x.id));
if (postList.isNotEmpty) { if (postList.isNotEmpty) {
var lastId = postList.map((x) => x.id).reduce(max); var lastId = postList.map((x) => x.id).reduce(max);
Get.find<LastReadProvider>().feedLastReadAt = lastId; Get.find<LastReadProvider>().feedLastReadAt = lastId;
@ -111,35 +120,39 @@ class PostListController extends GetxController {
Future<List<Post>?> _loadPosts(int pageKey) async { Future<List<Post>?> _loadPosts(int pageKey) async {
isBusy.value = true; isBusy.value = true;
final PostProvider provider = Get.find(); final PostProvider posts = Get.find();
Response resp; Response resp;
try { try {
if (author != null) { if (author != null) {
resp = await provider.listPost( resp = await posts.listPost(
pageKey, pageKey,
author: author, author: author,
take: 10,
); );
} else { } else {
switch (mode.value) { switch (mode.value) {
case 2: case 2:
resp = await provider.listRecommendations( resp = await posts.listRecommendations(
pageKey, pageKey,
channel: 'shuffle', channel: 'shuffle',
realm: realm, realm: realm,
take: 10,
); );
break; break;
case 1: case 1:
resp = await provider.listRecommendations( resp = await posts.listRecommendations(
pageKey, pageKey,
channel: 'friends', channel: 'friends',
realm: realm, realm: realm,
take: 10,
); );
break; break;
default: default:
resp = await provider.listRecommendations( resp = await posts.listRecommendations(
pageKey, pageKey,
realm: realm, realm: realm,
take: 10,
); );
break; break;
} }
@ -153,6 +166,27 @@ class PostListController extends GetxController {
final result = PaginationResult.fromJson(resp.body); final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => Post.fromJson(e)).toList(); final out = result.data?.map((e) => Post.fromJson(e)).toList();
final AttachmentProvider attach = Get.find();
if (out != null) {
final attachmentIds = out
.mapMany((x) => x.body['attachments'] ?? [])
.cast<String>()
.toSet()
.toList();
final attachmentOut = await attach.listMetadata(attachmentIds);
for (var idx = 0; idx < out.length; idx++) {
final rids = List<String>.from(out[idx].body['attachments'] ?? []);
out[idx].preload = PostPreload(
attachments: attachmentOut
.where((x) => x != null && rids.contains(x.rid))
.cast<Attachment>()
.toList(),
);
}
}
postTotal.value = result.count; postTotal.value = result.count;
return out; return out;

View File

@ -1,10 +1,19 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/post_categories.dart'; import 'package:solian/models/post_categories.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
part 'post.g.dart'; part 'post.g.dart';
class PostPreload {
List<Attachment> attachments;
PostPreload({
required this.attachments,
});
}
@JsonSerializable() @JsonSerializable()
class Post { class Post {
int id; int id;
@ -33,6 +42,9 @@ class Post {
Account author; Account author;
PostMetric? metric; PostMetric? metric;
@JsonKey(includeFromJson: false, includeToJson: false)
PostPreload? preload;
Post({ Post({
required this.id, required this.id,
required this.createdAt, required this.createdAt,

View File

@ -125,7 +125,7 @@ class AuthProvider extends GetConnect {
userAgent: await ServiceFinder.getUserAgent(), userAgent: await ServiceFinder.getUserAgent(),
sendUserAgent: true, sendUserAgent: true,
); );
client.httpClient.addAuthenticator(requestAuthenticator); client.httpClient.addRequestModifier(requestAuthenticator);
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null); client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
return client; return client;

View File

@ -41,25 +41,27 @@ class AttachmentProvider extends GetConnect {
} }
} }
final resp = await get( if (pendingQuery.isNotEmpty) {
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}', final resp = await get(
); '/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
if (resp.statusCode != 200) return result; );
if (resp.statusCode != 200) return result;
final rawOut = PaginationResult.fromJson(resp.body); final rawOut = PaginationResult.fromJson(resp.body);
if (rawOut.data == null) return result; if (rawOut.data == null) return result;
final List<Attachment> out = final List<Attachment> out =
rawOut.data!.map((x) => Attachment.fromJson(x)).toList(); rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) { for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) { if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.rid] = item; _cachedResponses[item.rid] = item;
}
} }
} for (var i = 0; i < out.length; i++) {
for (var i = 0; i < out.length; i++) { for (var j = 0; j < rid.length; j++) {
for (var j = 0; j < rid.length; j++) { if (out[i].rid == rid[j]) {
if (out[i].rid == rid[j]) { result[j] = out[i];
result[j] = out[i]; }
} }
} }
} }

View File

@ -3,22 +3,11 @@ import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/exceptions/unauthorized.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/services.dart';
class PostProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
}
class PostProvider extends GetxController {
Future<Response> seeWhatsNew(int pivot) async { Future<Response> seeWhatsNew(int pivot) async {
GetConnect client;
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.value) { final client = await auth.configureClient('co');
client = await auth.configureClient('co');
} else {
client = await ServiceFinder.configureClient('co');
}
final resp = await client.get('/whats-new?pivot=$pivot'); final resp = await client.get('/whats-new?pivot=$pivot');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
@ -28,19 +17,14 @@ class PostProvider extends GetConnect {
} }
Future<Response> listRecommendations(int page, Future<Response> listRecommendations(int page,
{String? realm, String? channel}) async { {String? realm, String? channel, int take = 10}) async {
GetConnect client;
final AuthProvider auth = Get.find();
final queries = [ final queries = [
'take=${10}', 'take=$take',
'offset=$page', 'offset=$page',
if (realm != null) 'realm=$realm', if (realm != null) 'realm=$realm',
]; ];
if (auth.isAuthorized.value) { final AuthProvider auth = Get.find();
client = await auth.configureClient('co'); final client = await auth.configureClient('interactive');
} else {
client = await ServiceFinder.configureClient('co');
}
final resp = await client.get( final resp = await client.get(
channel == null channel == null
? '/recommendations?${queries.join('&')}' ? '/recommendations?${queries.join('&')}'
@ -71,16 +55,18 @@ class PostProvider extends GetConnect {
} }
Future<Response> listPost(int page, Future<Response> listPost(int page,
{String? realm, String? author, tag, category}) async { {String? realm, String? author, tag, category, int take = 10}) async {
final queries = [ final queries = [
'take=${10}', 'take=$take',
'offset=$page', 'offset=$page',
if (tag != null) 'tag=$tag', if (tag != null) 'tag=$tag',
if (category != null) 'category=$category', if (category != null) 'category=$category',
if (author != null) 'author=$author', if (author != null) 'author=$author',
if (realm != null) 'realm=$realm', if (realm != null) 'realm=$realm',
]; ];
final resp = await get('/posts?${queries.join('&')}'); final AuthProvider auth = Get.find();
final client = await auth.configureClient('co');
final resp = await client.get('/posts?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -89,7 +75,10 @@ class PostProvider extends GetConnect {
} }
Future<Response> listPostReplies(String alias, int page) async { Future<Response> listPostReplies(String alias, int page) async {
final resp = await get('/posts/$alias/replies?take=${10}&offset=$page'); final AuthProvider auth = Get.find();
final client = await auth.configureClient('co');
final resp =
await client.get('/posts/$alias/replies?take=${10}&offset=$page');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -98,7 +87,9 @@ class PostProvider extends GetConnect {
} }
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async { Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
final resp = await get('/posts/$alias/replies/featured?take=$take'); final AuthProvider auth = Get.find();
final client = await auth.configureClient('co');
final resp = await client.get('/posts/$alias/replies/featured?take=$take');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -107,16 +98,9 @@ class PostProvider extends GetConnect {
} }
Future<Response> getPost(String alias) async { Future<Response> getPost(String alias) async {
final resp = await get('/posts/$alias'); final AuthProvider auth = Get.find();
if (resp.statusCode != 200) { final client = await auth.configureClient('co');
throw RequestException(resp); final resp = await client.get('/posts/$alias');
}
return resp;
}
Future<Response> getArticle(String alias) async {
final resp = await get('/articles/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }

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

@ -26,7 +26,6 @@ import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/daily_sign/history_chart.dart'; import 'package:solian/widgets/daily_sign/history_chart.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/reports/abuse_report.dart'; import 'package:solian/widgets/reports/abuse_report.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -261,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(
@ -609,7 +609,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
), ),
if (_userinfo != null) if (_userinfo != null)
PostWarpedListWidget( ControlledPostListWidget(
isPinned: false, isPinned: false,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),

View File

@ -69,6 +69,8 @@ class _ChatListState extends State<ChatList> {
late final ChannelProvider _channels = Get.find(); late final ChannelProvider _channels = Get.find();
bool _isBusy = true;
List<Channel> _sortChannels(List<Channel> channels) { List<Channel> _sortChannels(List<Channel> channels) {
channels.sort( channels.sort(
(a, b) => (a, b) =>
@ -117,18 +119,25 @@ class _ChatListState extends State<ChatList> {
final ctrl = ChatEventController(); final ctrl = ChatEventController();
await ctrl.initialize(); await ctrl.initialize();
final messages = await ctrl.src.getLastInAllChannels(); final messages = await ctrl.src.getLastInAllChannels();
setState(() { if (mounted) {
_lastMessages = messages setState(() {
.map((k, v) => MapEntry(k, v.firstOrNull)) _lastMessages = messages
.cast<int, LocalMessageEventTableData>(); .map((k, v) => MapEntry(k, v.firstOrNull))
}); .cast<int, LocalMessageEventTableData>();
});
}
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLastMessages().then((_) { _loadLastMessages().then((_) {
_loadAllChannels(); if (!mounted) return;
_loadAllChannels().then((_) {
if (mounted) {
setState(() => _isBusy = false);
}
});
}); });
} }
@ -143,16 +152,7 @@ class _ChatListState extends State<ChatList> {
child: ResponsiveRootContainer( child: ResponsiveRootContainer(
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
leading: Obx(() { leading: AppBarLeadingButton.adaptive(context),
final adaptive = AppBarLeadingButton.adaptive(context);
if (adaptive != null) return adaptive;
if (_channels.isLoading.value) {
return const CircularProgressIndicator(
strokeWidth: 3,
).paddingAll(18);
}
return const SizedBox.shrink();
}),
title: AppBarTitle('chat'.tr), title: AppBarTitle('chat'.tr),
centerTitle: true, centerTitle: true,
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
@ -252,7 +252,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,6 +280,26 @@ class _ChatListState extends State<ChatList> {
return Column( return Column(
children: [ children: [
const ChatCallCurrentIndicator(), const ChatCallCurrentIndicator(),
if (_isBusy)
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

@ -75,10 +75,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
final src = Get.find<MessagesFetchingProvider>(); final src = Get.find<MessagesFetchingProvider>();
final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!); final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!);
if (out == null) return; if (out == null) return;
setState(() { if (mounted) {
_currentMessages = out.$1; setState(() {
_currentMessagesCount = out.$2; _currentMessages = out.$1;
}); _currentMessagesCount = out.$2;
});
}
} }
bool _signingDaily = true; bool _signingDaily = true;
@ -390,7 +392,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
backgroundColor: Theme.of(context) backgroundColor: Theme.of(context)
.colorScheme .colorScheme
.surfaceContainerLow, .surfaceContainerLow,
), ).paddingAll(8),
), ),
), ),
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),

View File

@ -6,15 +6,15 @@ import 'package:get/get.dart';
import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart';
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_shuffle_swiper.dart'; import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
class ExploreScreen extends StatefulWidget { class ExploreScreen extends StatefulWidget {
@ -80,62 +80,85 @@ class _ExploreScreenState extends State<ExploreScreen>
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [ return [
SliverAppBar( SliverLayoutBuilder(
flexibleSpace: SizedBox( builder: (context, constraints) {
height: 48, final scrollOffset = constraints.scrollOffset;
child: const Row( final colorChangeOffset = 120;
children: [
RealmSwitcher(), final scrollProgress =
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
final backgroundColor = Color.lerp(
Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0),
Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0.9),
scrollProgress,
);
return SliverAppBar(
backgroundColor: backgroundColor,
flexibleSpace: SizedBox(
height: 48,
child: const Row(
children: [
RealmSwitcher(),
],
).paddingSymmetric(horizontal: 8),
).paddingOnly(top: MediaQuery.of(context).padding.top),
snap: true,
floating: true,
toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context),
actions: [
const BackgroundStateWidget(),
const NotificationButton(),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
], ],
).paddingSymmetric(horizontal: 8), bottom: TabBar(
).paddingOnly(top: MediaQuery.of(context).padding.top), controller: _tabController,
floating: true, dividerHeight: 0.3,
toolbarHeight: AppTheme.toolbarHeight(context), tabAlignment: TabAlignment.fill,
leading: AppBarLeadingButton.adaptive(context), tabs: [
actions: [ Tab(
const BackgroundStateWidget(), child: Row(
const NotificationButton(), mainAxisSize: MainAxisSize.min,
SizedBox( children: [
width: AppTheme.isLargeScreen(context) ? 8 : 16, const Icon(Icons.feed, size: 20),
), const Gap(8),
], Text('postListNews'.tr),
bottom: TabBar( ],
controller: _tabController, ),
dividerHeight: 0.3, ),
tabAlignment: TabAlignment.fill, Tab(
tabs: [ child: Row(
Tab( mainAxisSize: MainAxisSize.min,
child: Row( children: [
mainAxisSize: MainAxisSize.min, const Icon(Icons.people, size: 20),
children: [ const Gap(8),
const Icon(Icons.feed, size: 20), Text('postListFriends'.tr),
const Gap(8), ],
Text('postListNews'.tr), ),
], ),
), Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.shuffle_on_outlined, size: 20),
const Gap(8),
Text('postListShuffle'.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),
],
),
),
],
),
) )
]; ];
}, },
@ -156,7 +179,7 @@ class _ExploreScreenState extends State<ExploreScreen>
RefreshIndicator( RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
PostWarpedListWidget( ControlledPostListWidget(
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),
@ -167,7 +190,7 @@ class _ExploreScreenState extends State<ExploreScreen>
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
PostWarpedListWidget( ControlledPostListWidget(
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),
@ -202,106 +225,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

@ -3,7 +3,7 @@ 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/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/posts/post_warped_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import '../../models/post.dart'; import '../../models/post.dart';
@ -77,7 +77,7 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
onRefresh: () => Future.sync(() => _pagingController.refresh()), onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
PostWarpedListWidget( ControlledPostListWidget(
controller: _pagingController, controller: _pagingController,
onUpdate: () => _pagingController.refresh(), onUpdate: () => _pagingController.refresh(),
), ),

View File

@ -24,11 +24,11 @@ class PostDetailScreen extends StatefulWidget {
class _PostDetailScreenState extends State<PostDetailScreen> { class _PostDetailScreenState extends State<PostDetailScreen> {
Post? item; Post? item;
Future<Post?> getDetail() async { Future<Post?> _getDetail() async {
if (widget.post != null) { if (widget.post != null) {
item = widget.post; setState(() {
Get.find<LastReadProvider>().feedLastReadAt = item?.id; item = widget.post;
return widget.post; });
} }
final PostProvider provider = Get.find(); final PostProvider provider = Get.find();
@ -48,7 +48,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return FutureBuilder(
future: getDetail(), future: _getDetail(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) { if (!snapshot.hasData || snapshot.data == null) {
return const Center( return const Center(

View File

@ -273,116 +273,69 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
), ),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: Row( child: DefaultTabController(
crossAxisAlignment: CrossAxisAlignment.start, length: 2,
children: [ child: AppTheme.isLargeScreen(context)
Expanded( ? Row(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: ListView( child: _PostEditorTextField(
children: [ focusNode: _contentFocusNode,
Container( controller: _editorController,
padding: const EdgeInsets.symmetric( onUpdate: () => setState(() {}),
horizontal: 16, ),
vertical: 8, ),
), const VerticalDivider(width: 0.3, thickness: 0.3)
child: TextField( .paddingSymmetric(horizontal: 16),
maxLines: null, Expanded(
autofocus: true, child: SingleChildScrollView(
autocorrect: true, padding:
keyboardType: TextInputType.multiline, const EdgeInsets.only(top: 12, bottom: 64),
controller: child: MarkdownTextContent(
_editorController.contentController, isAutoWarp: _editorController.mode.value == 0,
focusNode: _contentFocusNode, content:
decoration: InputDecoration.collapsed( _editorController.contentController.text,
hintText: 'postContentPlaceholder'.tr, parentId: 'post-editor-preview',
), ).paddingOnly(right: 16),
onTapOutside: (_) => FocusManager ),
.instance.primaryFocus ),
?.unfocus(), ],
), )
), : Column(
const Gap(120) children: [
TabBar(
tabs: [
const Tab(icon: Icon(Icons.edit)),
const Tab(icon: Icon(Icons.preview)),
], ],
), ),
), Expanded(
Obx(() { child: TabBarView(
final textStyle = TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
);
final showFactors = [
_editorController.isRestoreFromLocal.value,
_editorController.lastSaveTime.value != null,
];
final doShow = showFactors.any((x) => x);
return Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 16,
),
child: Row(
children: [ children: [
if (showFactors[0]) _PostEditorTextField(
Text('postRestoreFromLocal'.tr, focusNode: _contentFocusNode,
style: textStyle) controller: _editorController,
.paddingOnly(right: 4), onUpdate: () => setState(() {}),
if (showFactors[0]) ),
InkWell( SingleChildScrollView(
child: Text('clear'.tr, style: textStyle), padding: const EdgeInsets.only(
onTap: () { top: 12,
_editorController.localClear(); bottom: 64,
_editorController.currentClear();
setState(() {});
},
),
if (showFactors.where((x) => x).length > 1)
Text(
'·',
style: textStyle,
).paddingSymmetric(horizontal: 8),
if (showFactors[1])
Text(
'postAutoSaveAt'.trParams({
'date': DateFormat('HH:mm:ss').format(
_editorController.lastSaveTime.value ??
DateTime.now(),
)
}),
style: textStyle,
), ),
child: MarkdownTextContent(
isAutoWarp:
_editorController.mode.value == 0,
content: _editorController
.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(left: 16, right: 16),
)
], ],
), ),
) ),
.animate( ],
key: const Key('post-editor-hint-animation'),
target: doShow ? 1 : 0,
)
.fade(curve: Curves.easeInOut, duration: 300.ms);
}),
],
),
),
if (AppTheme.isLargeScreen(context))
const VerticalDivider(width: 0.3, thickness: 0.3)
.paddingSymmetric(
horizontal: 16,
),
if (AppTheme.isLargeScreen(context))
Expanded(
child: SingleChildScrollView(
child: MarkdownTextContent(
isAutoWarp: _editorController.mode.value == 0,
content: _editorController.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(top: 12, right: 16),
), ),
),
],
), ),
), ),
Material( Material(
@ -391,6 +344,26 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Divider(thickness: 0.3, height: 0.3), const Divider(thickness: 0.3, height: 0.3),
SizedBox(
height: 40,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: MarkdownToolbar(
width: 38,
height: 38,
iconSize: 20,
spacing: 8,
hideImage: true,
useIncludedTextField: false,
backgroundColor: Theme.of(context).colorScheme.surface,
iconColor: Theme.of(context).colorScheme.onSurface,
controller: _editorController.contentController,
focusNode: _contentFocusNode,
borderRadius:
const BorderRadius.all(Radius.circular(20)),
).paddingSymmetric(horizontal: 12),
),
).paddingOnly(top: 12),
SizedBox( SizedBox(
height: 56, height: 56,
child: ListView( child: ListView(
@ -520,7 +493,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
top: -4, top: -4,
end: -6, end: -6,
), ),
child: const Icon(Icons.preview), child: const Icon(Icons.wallpaper),
); );
}), }),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@ -547,18 +520,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
_editorController.editPublishDate(context); _editorController.editPublishDate(context);
}, },
), ),
MarkdownToolbar(
hideImage: true,
useIncludedTextField: false,
backgroundColor:
Theme.of(context).colorScheme.surface,
iconColor: Theme.of(context).colorScheme.onSurface,
controller: _editorController.contentController,
focusNode: _contentFocusNode,
borderRadius:
const BorderRadius.all(Radius.circular(20)),
width: 40,
).paddingSymmetric(horizontal: 2),
], ],
).paddingSymmetric(horizontal: 6, vertical: 8), ).paddingSymmetric(horizontal: 6, vertical: 8),
), ),
@ -578,3 +539,101 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
super.dispose(); super.dispose();
} }
} }
class _PostEditorTextField extends StatelessWidget {
final FocusNode focusNode;
final PostEditorController controller;
final Function onUpdate;
const _PostEditorTextField({
required this.focusNode,
required this.controller,
required this.onUpdate,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: TextField(
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: controller.contentController,
focusNode: focusNode,
decoration: InputDecoration.collapsed(
hintText: 'postContentPlaceholder'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(120)
],
),
),
Obx(() {
final textStyle = TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
);
final showFactors = [
controller.isRestoreFromLocal.value,
controller.lastSaveTime.value != null,
];
final doShow = showFactors.any((x) => x);
return Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 16,
),
child: Row(
children: [
if (showFactors[0])
Text('postRestoreFromLocal'.tr, style: textStyle)
.paddingOnly(right: 4),
if (showFactors[0])
InkWell(
child: Text('clear'.tr, style: textStyle),
onTap: () {
controller.localClear();
controller.currentClear();
onUpdate();
},
),
if (showFactors.where((x) => x).length > 1)
Text(
'·',
style: textStyle,
).paddingSymmetric(horizontal: 8),
if (showFactors[1])
Text(
'postAutoSaveAt'.trParams({
'date': DateFormat('HH:mm:ss').format(
controller.lastSaveTime.value ?? DateTime.now(),
)
}),
style: textStyle,
),
],
),
)
.animate(
key: const Key('post-editor-hint-animation'),
target: doShow ? 1 : 0,
)
.fade(curve: Curves.easeInOut, duration: 300.ms);
}),
],
);
}
}

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

@ -49,6 +49,7 @@ class RootShell extends StatelessWidget {
return Scaffold( return Scaffold(
key: rootScaffoldKey, key: rootScaffoldKey,
backgroundColor: Theme.of(context).colorScheme.surface,
bottomNavigationBar: showBottomNavigation bottomNavigationBar: showBottomNavigation
? AppNavigationBottom( ? AppNavigationBottom(
initialIndex: destNames.indexOf(routeName ?? 'page'), initialIndex: destNames.indexOf(routeName ?? 'page'),

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

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

@ -15,12 +15,12 @@ import 'package:solian/widgets/sized_container.dart';
class AttachmentList extends StatefulWidget { class AttachmentList extends StatefulWidget {
final String parentId; final String parentId;
final List<String> attachmentsId; final List<String>? attachmentIds;
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 double? width; final double? width;
@ -29,12 +29,12 @@ class AttachmentList extends StatefulWidget {
const AttachmentList({ const AttachmentList({
super.key, super.key,
required this.parentId, required this.parentId,
required this.attachmentsId, this.attachmentIds,
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.width, this.width,
this.viewport, this.viewport,
@ -50,21 +50,21 @@ class _AttachmentListState extends State<AttachmentList> {
double _aspectRatio = 1; double _aspectRatio = 1;
List<Attachment?> _attachmentsMeta = List.empty(); List<Attachment?> _attachments = List.empty();
void _getMetadataList() { void _getMetadataList() {
final AttachmentProvider attach = Get.find(); final AttachmentProvider attach = Get.find();
if (widget.attachmentsId.isEmpty) { if (widget.attachmentIds?.isEmpty ?? false) {
return; return;
} else { } else {
_attachmentsMeta = List.filled(widget.attachmentsId.length, null); _attachments = List.filled(widget.attachmentIds!.length, null);
} }
attach.listMetadata(widget.attachmentsId).then((result) { attach.listMetadata(widget.attachmentIds!).then((result) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_attachmentsMeta = result; _attachments = result;
_isLoading = false; _isLoading = false;
}); });
} }
@ -76,7 +76,7 @@ class _AttachmentListState extends State<AttachmentList> {
bool isConsistent = true; bool isConsistent = true;
double? consistentValue; double? consistentValue;
int portrait = 0, square = 0, landscape = 0; int portrait = 0, square = 0, landscape = 0;
for (var entry in _attachmentsMeta) { for (var entry in _attachments) {
if (entry == null) continue; if (entry == null) continue;
if (entry.metadata?['ratio'] != null) { if (entry.metadata?['ratio'] != null) {
if (entry.metadata?['ratio'] is int) { if (entry.metadata?['ratio'] is int) {
@ -117,10 +117,9 @@ class _AttachmentListState extends State<AttachmentList> {
item: element, item: element,
parentId: widget.parentId, parentId: widget.parentId,
width: width ?? widget.width, width: width ?? widget.width,
badgeContent: '${idx + 1}/${_attachmentsMeta.length}', badgeContent: '${idx + 1}/${_attachments.length}',
showBadge: showBadge: _attachments.length > 1 && !widget.isGrid && !widget.isColumn,
_attachmentsMeta.length > 1 && !widget.isGrid && !widget.isColumn, showBorder: _attachments.length > 1,
showBorder: widget.attachmentsId.length > 1,
showMature: _showMature, showMature: _showMature,
autoload: widget.autoload, autoload: widget.autoload,
onReveal: (value) { onReveal: (value) {
@ -132,7 +131,16 @@ class _AttachmentListState extends State<AttachmentList> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_getMetadataList(); assert(widget.attachmentIds != null || widget.attachments != null);
if (widget.attachments == null) {
_getMetadataList();
} else {
setState(() {
_attachments = widget.attachments!;
_isLoading = false;
});
_calculateAspectRatio();
}
} }
Color get _unFocusColor => Color get _unFocusColor =>
@ -140,7 +148,7 @@ class _AttachmentListState extends State<AttachmentList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.attachmentsId.isEmpty) { if (widget.attachmentIds?.isEmpty ?? widget.attachments!.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -154,7 +162,7 @@ class _AttachmentListState extends State<AttachmentList> {
).paddingOnly(right: 5), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams( 'attachmentHint'.trParams(
{'count': widget.attachmentsId.length.toString()}, {'count': _attachments.toString()},
), ),
style: TextStyle(color: _unFocusColor, fontSize: 12), style: TextStyle(color: _unFocusColor, fontSize: 12),
) )
@ -165,14 +173,75 @@ 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;
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container(
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
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.surfaceContainerHigh,
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,
children: widget.attachmentsId.map((x) { children: _attachments.map((x) {
final element = _attachmentsMeta[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; double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
@ -202,68 +271,44 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final isNotPureImage = _attachmentsMeta.any( return SizedBox(
(x) => x?.mimetype.split('/').firstOrNull != 'image', width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth),
);
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
const radius = BorderRadius.all(Radius.circular(8));
return GridView.builder(
padding: EdgeInsets.zero,
primary: false,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: math.min(3, widget.attachmentsId.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
itemCount: widget.attachmentsId.length,
itemBuilder: (context, idx) {
final element = _attachmentsMeta[idx];
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
);
},
).paddingSymmetric(horizontal: 24);
}
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( child: CarouselSlider.builder(
options: CarouselOptions( options: CarouselOptions(
disableCenter: true,
animateToClosest: true, animateToClosest: true,
aspectRatio: _aspectRatio, aspectRatio: _aspectRatio,
viewportFraction: enlargeCenterPage: true,
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1), viewportFraction: widget.viewport ?? 0.95,
enableInfiniteScroll: false, enableInfiniteScroll: false,
), ),
itemCount: _attachmentsMeta.length, itemCount: _attachments.length,
itemBuilder: (context, idx, _) { itemBuilder: (context, idx, _) {
final element = _attachmentsMeta[idx]; final element = _attachments[idx];
return _buildEntry(element, idx); if (element == null) const SizedBox.shrink();
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container(
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _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

@ -78,7 +78,7 @@ class ChatEvent extends StatelessWidget {
child: AttachmentList( child: AttachmentList(
key: Key('m${item.uuid}attachments'), key: Key('m${item.uuid}attachments'),
parentId: item.uuid, parentId: item.uuid,
attachmentsId: attachments, attachmentIds: attachments,
isColumn: true, isColumn: true,
), ),
); );
@ -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

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

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

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

@ -12,7 +12,6 @@ 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';
@ -36,6 +35,7 @@ class PostItem extends StatefulWidget {
final bool showFeaturedReply; final bool showFeaturedReply;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor; final Color? backgroundColor;
final Function? onComment;
const PostItem({ const PostItem({
super.key, super.key,
@ -52,6 +52,7 @@ class PostItem extends StatefulWidget {
this.showFeaturedReply = false, this.showFeaturedReply = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.backgroundColor,
this.onComment,
}); });
@override @override
@ -92,32 +93,27 @@ class _PostItemState extends State<PostItem> {
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( maxHeight: widget.isFullContent ? double.infinity : 80,
maxWidth: 640, child: _MeasureSize(
maxHeight: widget.isFullContent ? double.infinity : 80, onChange: (size) {
child: _MeasureSize( setState(() => _contentHeight = size.height);
onChange: (size) { },
setState(() => _contentHeight = size.height); child: SingleChildScrollView(
}, physics: const NeverScrollableScrollPhysics(),
child: SingleChildScrollView( child: MarkdownTextContent(
physics: const NeverScrollableScrollPhysics(), parentId: 'p${item.id}',
child: MarkdownTextContent( content: item.body['content'],
parentId: 'p${item.id}', isAutoWarp: item.type == 'story',
content: item.body['content'], isSelectable: widget.isContentSelectable,
isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable,
),
).paddingOnly(
left: 16,
right: 12,
top: 2,
bottom: hasAttachment ? 4 : 0,
),
), ),
).paddingOnly(
left: 12,
right: 12,
bottom: hasAttachment ? 4 : 0,
), ),
], ),
), ),
if (_contentHeight >= 80 && !widget.isFullContent) if (_contentHeight >= 80 && !widget.isFullContent)
Opacity( Opacity(
@ -132,7 +128,7 @@ class _PostItemState extends State<PostItem> {
right: 8, right: 8,
top: 4, top: 4,
), ),
_PostFooterWidget(item: item).paddingOnly(left: 16), _PostFooterWidget(item: item).paddingOnly(left: 12),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
Row( Row(
children: [ children: [
@ -148,7 +144,7 @@ class _PostItemState extends State<PostItem> {
style: TextStyle(color: _unFocusColor), style: TextStyle(color: _unFocusColor),
) )
], ],
).paddingOnly(left: 16, top: 4), ).paddingOnly(left: 14, top: 4),
], ],
); );
} }
@ -162,113 +158,80 @@ class _PostItemState extends State<PostItem> {
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: () { item: item,
showModalBottomSheet(
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: [ maxHeight: widget.isFullContent ? double.infinity : 320,
_PostHeaderWidget( child: _MeasureSize(
isCompact: widget.isCompact, onChange: (size) {
item: item, 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,
), ),
_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 (_contentHeight >= 320 && !widget.isFullContent)
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(top: 4),
], ],
).paddingOnly( ).paddingOnly(
top: 10,
bottom:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 0,
right: 16, right: 16,
left: 16, left: 16,
), ),
_PostAttachmentWidget(item: item), _PostAttachmentWidget(item: item),
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item), if (widget.showFeaturedReply)
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
horizontal: 12,
),
if (widget.showFeaturedReply) const Gap(8),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,
@ -280,19 +243,15 @@ 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)) left: 14,
? 10 right: 14,
: 6,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
bottom: 10,
) )
else
const Gap(10),
], ],
), ),
openBuilder: (_, __) => TitleShell( openBuilder: (_, __) => TitleShell(
@ -317,7 +276,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,13 +283,10 @@ 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();
@ -351,7 +306,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 +378,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,
); );
}, },
); );
@ -452,27 +400,33 @@ 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) {
return AttachmentList( return AttachmentList(
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentsId: attachments, attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
autoload: false,
isFullWidth: true,
).paddingOnly(top: 4);
} else if (attachments.length > 1 &&
attachments.length % 3 == 0 &&
!isLargeScreen) {
return AttachmentList(
parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
autoload: false, autoload: false,
isGrid: true, isGrid: true,
).paddingOnly(left: 36, top: 4, bottom: 4); ).paddingSymmetric(horizontal: 14, vertical: 8);
} else if (attachments.length > 1 || isLargeScreen) {
return AttachmentList(
parentId: item.id.toString(),
attachmentsId: attachments,
autoload: false,
isColumn: true,
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
} else { } else {
return AttachmentList( return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentsId: attachments, attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
autoload: false, autoload: false,
); ).paddingOnly(bottom: 8, top: 4);
} }
} }
} }
@ -512,16 +466,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(
@ -557,9 +512,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();
} }
@ -631,48 +584,58 @@ class _PostHeaderWidget extends StatelessWidget {
@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(),
).paddingOnly(top: 1),
],
), ),
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()) if (!isCompact)
.paddingOnly(left: 4), RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()),
], ],
), ),
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,
),
], ],
); );
} }

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,19 @@ 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(); backgroundColor: backgroundColor,
}, onUpdate: () {
controller.refresh();
},
),
); );
}, },
), ),
@ -48,7 +55,6 @@ class PostListWidget extends StatelessWidget {
} }
class PostListEntryWidget extends StatelessWidget { class PostListEntryWidget extends StatelessWidget {
final int renderOrder;
final bool isShowEmbed; final bool isShowEmbed;
final bool isNestedClickable; final bool isNestedClickable;
final bool isClickable; final bool isClickable;
@ -59,7 +65,6 @@ class PostListEntryWidget extends StatelessWidget {
const PostListEntryWidget({ const PostListEntryWidget({
super.key, super.key,
this.renderOrder = 0,
required this.isShowEmbed, required this.isShowEmbed,
required this.isNestedClickable, required this.isNestedClickable,
required this.isClickable, required this.isClickable,
@ -79,6 +84,22 @@ class PostListEntryWidget extends StatelessWidget {
isClickable: isNestedClickable, isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply, showFeaturedReply: showFeaturedReply,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
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();
@ -101,3 +122,46 @@ class PostListEntryWidget extends StatelessWidget {
); );
} }
} }
class ControlledPostListWidget extends StatelessWidget {
final bool isShowEmbed;
final bool isClickable;
final bool isNestedClickable;
final bool isPinned;
final PagingController<int, Post> controller;
final Function? onUpdate;
const ControlledPostListWidget({
super.key,
required this.controller,
this.isShowEmbed = true,
this.isClickable = true,
this.isNestedClickable = true,
this.isPinned = true,
this.onUpdate,
});
@override
Widget build(BuildContext context) {
return PagedSliverList<int, Post>.separated(
addRepaintBoundaries: true,
pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) {
if (item.pinnedAt != null && !isPinned) {
return const SizedBox.shrink();
}
return PostListEntryWidget(
isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable,
isClickable: isClickable,
showFeaturedReply: true,
item: item,
onUpdate: onUpdate ?? () {},
);
},
),
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
);
}
}

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

@ -53,6 +53,7 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PostListWidget( return PostListWidget(
padding: EdgeInsets.symmetric(horizontal: 10),
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
@ -70,16 +71,30 @@ 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, backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
), ),
], ],
), ),

View File

@ -1,48 +0,0 @@
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_list.dart';
class PostWarpedListWidget extends StatelessWidget {
final bool isShowEmbed;
final bool isClickable;
final bool isNestedClickable;
final bool isPinned;
final PagingController<int, Post> controller;
final Function? onUpdate;
const PostWarpedListWidget({
super.key,
required this.controller,
this.isShowEmbed = true,
this.isClickable = true,
this.isNestedClickable = true,
this.isPinned = true,
this.onUpdate,
});
@override
Widget build(BuildContext context) {
return PagedSliverList<int, Post>.separated(
addRepaintBoundaries: true,
pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) {
if (item.pinnedAt != null && !isPinned) {
return const SizedBox.shrink();
}
return PostListEntryWidget(
renderOrder: index,
isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable,
isClickable: isClickable,
showFeaturedReply: true,
item: item,
onUpdate: onUpdate ?? () {},
);
},
),
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
);
}
}

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

@ -29,7 +29,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

View File

@ -195,7 +195,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)":
@ -249,7 +249,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`)
@ -328,8 +328,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:
@ -340,7 +340,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 device_info_plus: f1aae8670672f75c4c8850ecbe0b2ddef62b0a22
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
firebase_analytics: 30ff72f6d4847ff0b479d8edd92fc8582e719072 firebase_analytics: 30ff72f6d4847ff0b479d8edd92fc8582e719072
@ -371,16 +371,16 @@ 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
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

@ -346,10 +346,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: device_info_plus name: device_info_plus
sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 sha256: db03b2d2a3fa466a4627709e1db58692c3f7f658e36a5942d342d86efedc4091
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.2" version: "11.0.0"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -450,10 +450,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 +695,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:
@ -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:
@ -1769,18 +1769,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 +1902,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:
@ -2062,10 +2086,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:

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.6+5 version: 1.3.7+8
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -38,7 +38,7 @@ dependencies:
firebase_core: ^3.0.0 firebase_core: ^3.0.0
firebase_messaging: ^15.0.0 firebase_messaging: ^15.0.0
package_info_plus: ^8.0.0 package_info_plus: ^8.0.0
device_info_plus: ^10.1.0 device_info_plus: ^11.0.0
flutter_acrylic: ^1.1.4 flutter_acrylic: ^1.1.4
protocol_handler: ^0.2.0 protocol_handler: ^0.2.0
markdown: ^7.2.2 markdown: ^7.2.2

9
roadsign.toml Normal file
View File

@ -0,0 +1,9 @@
id = "solian"
[[locations]]
id = "solian"
host = ["sn.solsynth.dev"]
path = ["/"]
[[locations.destinations]]
id = "solian-web"
uri = "files:///workdir/solian?fallback=index.html&index=index.html"