Compare commits
6 Commits
46919dec31
...
3.0.0+111
Author | SHA1 | Date | |
---|---|---|---|
|
552b4b2572 | ||
|
594ac39e3d | ||
|
23321171f3 | ||
|
ee72d79c93 | ||
|
a20c2598fc | ||
|
2eba871a6d |
@@ -375,7 +375,9 @@
|
|||||||
"postContent": "Content",
|
"postContent": "Content",
|
||||||
"postSettings": "Settings",
|
"postSettings": "Settings",
|
||||||
"postPublisherUnselected": "Publisher Unspecified",
|
"postPublisherUnselected": "Publisher Unspecified",
|
||||||
"postVisibility": "Visibility",
|
"postType": "Post Type",
|
||||||
|
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
||||||
|
"postVisibility": "Post Visibility",
|
||||||
"postVisibilityPublic": "Public",
|
"postVisibilityPublic": "Public",
|
||||||
"postVisibilityFriends": "Friends Only",
|
"postVisibilityFriends": "Friends Only",
|
||||||
"postVisibilityUnlisted": "Unlisted",
|
"postVisibilityUnlisted": "Unlisted",
|
||||||
|
@@ -532,7 +532,7 @@
|
|||||||
"aboutScreenContactUsTitle": "联系我们",
|
"aboutScreenContactUsTitle": "联系我们",
|
||||||
"aboutScreenLicenseTitle": "许可证",
|
"aboutScreenLicenseTitle": "许可证",
|
||||||
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
|
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
|
||||||
"aboutScreenCopyright": "版权所有 © Solsynth {}",
|
"aboutScreenCopyright": "版权所有 © 索尔辛茨 {}",
|
||||||
"aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作",
|
"aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作",
|
||||||
"aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}",
|
"aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}",
|
||||||
"copiedToClipboard": "已复制到剪贴板",
|
"copiedToClipboard": "已复制到剪贴板",
|
||||||
|
@@ -221,7 +221,7 @@ class IslandApp extends HookConsumerWidget {
|
|||||||
Future(() {
|
Future(() {
|
||||||
userNotifier.fetchUser().then((_) {
|
userNotifier.fetchUser().then((_) {
|
||||||
final user = ref.watch(userInfoProvider);
|
final user = ref.watch(userInfoProvider);
|
||||||
if (user.hasValue) {
|
if (user.value != null) {
|
||||||
final apiClient = ref.read(apiClientProvider);
|
final apiClient = ref.read(apiClientProvider);
|
||||||
subscribePushNotification(apiClient);
|
subscribePushNotification(apiClient);
|
||||||
final wsNotifier = ref.read(websocketStateProvider.notifier);
|
final wsNotifier = ref.read(websocketStateProvider.notifier);
|
||||||
|
@@ -18,8 +18,13 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
|||||||
final user = SnAccount.fromJson(response.data);
|
final user = SnAccount.fromJson(response.data);
|
||||||
state = AsyncValue.data(user);
|
state = AsyncValue.data(user);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
log("[UserInfo] Failed to fetch user info: $error");
|
log(
|
||||||
state = AsyncValue.error(error, stackTrace);
|
"[UserInfo] Failed to fetch user info...",
|
||||||
|
name: 'UserInfoNotifier',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
state = AsyncValue.data(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -59,7 +59,7 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
notificationUnreadCountNotifierProvider,
|
notificationUnreadCountNotifierProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user.hasValue || user.value == null) {
|
if (user.value == null || user.value == null) {
|
||||||
return _UnauthorizedAccountScreen();
|
return _UnauthorizedAccountScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,12 +367,23 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
TextButton(
|
Row(
|
||||||
onPressed: () {
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
context.push('/settings');
|
children: [
|
||||||
},
|
TextButton(
|
||||||
child: Text('appSettings').tr(),
|
onPressed: () {
|
||||||
).center(),
|
context.push('/about');
|
||||||
|
},
|
||||||
|
child: Text('about').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.push('/settings');
|
||||||
|
},
|
||||||
|
child: Text('appSettings').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).center(),
|
).center(),
|
||||||
|
@@ -82,7 +82,7 @@ class EventCalanderScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Show user profile if viewing someone else's calendar
|
// Show user profile if viewing someone else's calendar
|
||||||
if (name != 'me' && user.hasValue)
|
if (name != 'me' && user.value != null)
|
||||||
AccountNameplate(name: name),
|
AccountNameplate(name: name),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -106,7 +106,7 @@ class EventCalanderScreen extends HookConsumerWidget {
|
|||||||
).padding(horizontal: 8, vertical: 4),
|
).padding(horizontal: 8, vertical: 4),
|
||||||
|
|
||||||
// Show user profile if viewing someone else's calendar
|
// Show user profile if viewing someone else's calendar
|
||||||
if (name != 'me' && user.hasValue)
|
if (name != 'me' && user.value != null)
|
||||||
AccountNameplate(name: name),
|
AccountNameplate(name: name),
|
||||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
@@ -72,6 +72,8 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
|
Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
if (userInfo.value == null) return null;
|
||||||
final account = await ref.watch(accountProvider(uname).future);
|
final account = await ref.watch(accountProvider(uname).future);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
try {
|
try {
|
||||||
@@ -87,6 +89,8 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
|
Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
|
||||||
|
final userInfo = ref.watch(userInfoProvider);
|
||||||
|
if (userInfo.value == null) return null;
|
||||||
final account = await ref.watch(accountProvider(uname).future);
|
final account = await ref.watch(accountProvider(uname).future);
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
try {
|
try {
|
||||||
@@ -219,6 +223,8 @@ class AccountProfileScreen extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final user = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
return account.when(
|
return account.when(
|
||||||
data:
|
data:
|
||||||
(data) => AppScaffold(
|
(data) => AppScaffold(
|
||||||
@@ -379,56 +385,60 @@ class AccountProfileScreen extends HookConsumerWidget {
|
|||||||
).padding(horizontal: 24),
|
).padding(horizontal: 24),
|
||||||
),
|
),
|
||||||
|
|
||||||
SliverToBoxAdapter(
|
if (user.value != null)
|
||||||
child: const Divider(height: 1).padding(top: 24, bottom: 12),
|
SliverToBoxAdapter(
|
||||||
),
|
child: const Divider(
|
||||||
SliverToBoxAdapter(
|
height: 1,
|
||||||
child: Row(
|
).padding(top: 24, bottom: 12),
|
||||||
spacing: 8,
|
),
|
||||||
children: [
|
if (user.value != null)
|
||||||
Expanded(
|
SliverToBoxAdapter(
|
||||||
child: FilledButton.icon(
|
child: Row(
|
||||||
style: ButtonStyle(
|
spacing: 8,
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
children: [
|
||||||
accountRelationship.value == null
|
Expanded(
|
||||||
? null
|
child: FilledButton.icon(
|
||||||
: Theme.of(context).colorScheme.secondary,
|
style: ButtonStyle(
|
||||||
),
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
foregroundColor: WidgetStatePropertyAll(
|
|
||||||
accountRelationship.value == null
|
|
||||||
? null
|
|
||||||
: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: relationshipAction,
|
|
||||||
label:
|
|
||||||
Text(
|
|
||||||
accountRelationship.value == null
|
accountRelationship.value == null
|
||||||
? 'addFriendShort'
|
? null
|
||||||
: 'added',
|
: Theme.of(context).colorScheme.secondary,
|
||||||
).tr(),
|
),
|
||||||
icon:
|
foregroundColor: WidgetStatePropertyAll(
|
||||||
accountRelationship.value == null
|
accountRelationship.value == null
|
||||||
? const Icon(Symbols.person_add)
|
? null
|
||||||
: const Icon(Symbols.person_check),
|
: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: relationshipAction,
|
||||||
|
label:
|
||||||
|
Text(
|
||||||
|
accountRelationship.value == null
|
||||||
|
? 'addFriendShort'
|
||||||
|
: 'added',
|
||||||
|
).tr(),
|
||||||
|
icon:
|
||||||
|
accountRelationship.value == null
|
||||||
|
? const Icon(Symbols.person_add)
|
||||||
|
: const Icon(Symbols.person_check),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: FilledButton.icon(
|
||||||
child: FilledButton.icon(
|
onPressed: directMessageAction,
|
||||||
onPressed: directMessageAction,
|
icon: const Icon(Symbols.message),
|
||||||
icon: const Icon(Symbols.message),
|
label:
|
||||||
label:
|
Text(
|
||||||
Text(
|
accountChat.value == null
|
||||||
accountChat.value == null
|
? 'createDirectMessage'
|
||||||
? 'createDirectMessage'
|
: 'gotoDirectMessage',
|
||||||
: 'gotoDirectMessage',
|
maxLines: 1,
|
||||||
maxLines: 1,
|
).tr(),
|
||||||
).tr(),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
).padding(horizontal: 16),
|
||||||
).padding(horizontal: 16),
|
),
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: const Divider(height: 1).padding(top: 12),
|
child: const Divider(height: 1).padding(top: 12),
|
||||||
),
|
),
|
||||||
|
@@ -51,54 +51,59 @@ class _ArticleDetailContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: ConstrainedBox(
|
||||||
children: [
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
if (article.preview?.imageUrl != null)
|
child: Column(
|
||||||
Image.network(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
article.preview!.imageUrl!,
|
children: [
|
||||||
width: double.infinity,
|
if (article.preview?.imageUrl != null)
|
||||||
height: 200,
|
Image.network(
|
||||||
fit: BoxFit.cover,
|
article.preview!.imageUrl!,
|
||||||
),
|
width: double.infinity,
|
||||||
Padding(
|
height: 200,
|
||||||
padding: const EdgeInsets.all(16.0),
|
fit: BoxFit.cover,
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
article.title,
|
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
Padding(
|
||||||
if (article.feed?.title != null)
|
padding: const EdgeInsets.all(16.0),
|
||||||
Text(
|
child: Column(
|
||||||
article.feed!.title,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
children: [
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
Text(
|
||||||
|
article.title,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
const Divider(height: 32),
|
if (article.feed?.title != null)
|
||||||
if (article.content != null)
|
Text(
|
||||||
...MarkdownTextContent.buildGenerator(
|
article.feed!.title,
|
||||||
isDark: Theme.of(context).brightness == Brightness.dark,
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
).buildWidgets(markdownContent)
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
else if (article.preview?.description != null)
|
),
|
||||||
Text(article.preview!.description!),
|
|
||||||
const Gap(24),
|
|
||||||
FilledButton(
|
|
||||||
onPressed:
|
|
||||||
() => launchUrlString(
|
|
||||||
article.url,
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
),
|
),
|
||||||
child: const Text('Read Full Article'),
|
const Divider(height: 32),
|
||||||
|
if (article.content != null)
|
||||||
|
...MarkdownTextContent.buildGenerator(
|
||||||
|
isDark: Theme.of(context).brightness == Brightness.dark,
|
||||||
|
).buildWidgets(markdownContent)
|
||||||
|
else if (article.preview?.description != null)
|
||||||
|
Text(article.preview!.description!),
|
||||||
|
const Gap(24),
|
||||||
|
FilledButton(
|
||||||
|
onPressed:
|
||||||
|
() => launchUrlString(
|
||||||
|
article.url,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
),
|
||||||
|
child: const Text('Read Full Article'),
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Gap(MediaQuery.of(context).padding.bottom),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -126,16 +126,21 @@ class ArticlesScreen extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(title ?? 'Articles')),
|
appBar: AppBar(title: Text(title ?? 'Articles')),
|
||||||
body: CustomScrollView(
|
body: Center(
|
||||||
slivers: [
|
child: ConstrainedBox(
|
||||||
SliverPadding(
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
|
child: CustomScrollView(
|
||||||
sliver: SliverArticlesList(
|
slivers: [
|
||||||
feedId: feedId,
|
SliverPadding(
|
||||||
publisherId: publisherId,
|
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
|
||||||
),
|
sliver: SliverArticlesList(
|
||||||
|
feedId: feedId,
|
||||||
|
publisherId: publisherId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -307,7 +307,7 @@ class _ActivityListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
if (user.hasValue && !contentOnly)
|
if (user.value != null && !contentOnly)
|
||||||
SliverToBoxAdapter(child: CheckInWidget()),
|
SliverToBoxAdapter(child: CheckInWidget()),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: widgetCount,
|
itemCount: widgetCount,
|
||||||
|
@@ -321,8 +321,15 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
builder: (context, attachments, _) {
|
builder: (context, attachments, _) {
|
||||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
|
Text(
|
||||||
|
'articleAttachmentHint'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
).padding(bottom: 8),
|
||||||
ValueListenableBuilder<Map<int, double>>(
|
ValueListenableBuilder<Map<int, double>>(
|
||||||
valueListenable: state.attachmentProgress,
|
valueListenable: state.attachmentProgress,
|
||||||
builder: (context, progressMap, _) {
|
builder: (context, progressMap, _) {
|
||||||
@@ -332,8 +339,8 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
for (var idx = 0; idx < attachments.length; idx++)
|
for (var idx = 0; idx < attachments.length; idx++)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 280,
|
||||||
height: 120,
|
height: 280,
|
||||||
child: AttachmentPreview(
|
child: AttachmentPreview(
|
||||||
item: attachments[idx],
|
item: attachments[idx],
|
||||||
progress: progressMap[idx],
|
progress: progressMap[idx],
|
||||||
@@ -358,6 +365,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
delta,
|
delta,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onInsert:
|
||||||
|
() => ComposeLogic.insertAttachment(
|
||||||
|
ref,
|
||||||
|
state,
|
||||||
|
idx,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@@ -59,7 +59,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
|
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
|
||||||
);
|
);
|
||||||
if (user.hasValue) {
|
if (user.value != null) {
|
||||||
ref.invalidate(accountStatusProvider(user.value!.name));
|
ref.invalidate(accountStatusProvider(user.value!.name));
|
||||||
}
|
}
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
@@ -350,7 +350,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
|
|||||||
return AnimatedPositioned(
|
return AnimatedPositioned(
|
||||||
duration: Duration(milliseconds: 1850),
|
duration: Duration(milliseconds: 1850),
|
||||||
top:
|
top:
|
||||||
!user.hasValue ||
|
user.value == null ||
|
||||||
user.value == null ||
|
user.value == null ||
|
||||||
websocketState == WebSocketState.connected()
|
websocketState == WebSocketState.connected()
|
||||||
? -indicatorHeight
|
? -indicatorHeight
|
||||||
@@ -362,7 +362,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
|
|||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation:
|
elevation:
|
||||||
!user.hasValue || websocketState == WebSocketState.connected()
|
user.value == null || websocketState == WebSocketState.connected()
|
||||||
? 0
|
? 0
|
||||||
: 4,
|
: 4,
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
|
@@ -15,6 +15,7 @@ class AttachmentPreview extends StatelessWidget {
|
|||||||
final double? progress;
|
final double? progress;
|
||||||
final Function(int)? onMove;
|
final Function(int)? onMove;
|
||||||
final Function? onDelete;
|
final Function? onDelete;
|
||||||
|
final Function? onInsert;
|
||||||
final Function? onRequestUpload;
|
final Function? onRequestUpload;
|
||||||
const AttachmentPreview({
|
const AttachmentPreview({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -23,6 +24,7 @@ class AttachmentPreview extends StatelessWidget {
|
|||||||
this.onRequestUpload,
|
this.onRequestUpload,
|
||||||
this.onMove,
|
this.onMove,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
|
this.onInsert,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -104,7 +106,11 @@ class AttachmentPreview extends StatelessWidget {
|
|||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
Gap(6),
|
Gap(6),
|
||||||
Center(child: LinearProgressIndicator(value: progress)),
|
Center(
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress != null ? progress! / 100.0 : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -166,6 +172,18 @@ class AttachmentPreview extends StatelessWidget {
|
|||||||
onMove?.call(1);
|
onMove?.call(1);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (onInsert != null)
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.add,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
).padding(horizontal: 8, vertical: 6),
|
||||||
|
onTap: () {
|
||||||
|
onInsert?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -6,11 +7,14 @@ import 'package:flutter_highlight/themes/a11y-dark.dart';
|
|||||||
import 'package:flutter_highlight/themes/a11y-light.dart';
|
import 'package:flutter_highlight/themes/a11y-light.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown_latex.dart';
|
import 'package:island/widgets/content/markdown_latex.dart';
|
||||||
import 'package:markdown/markdown.dart' as markdown;
|
import 'package:markdown/markdown.dart' as markdown;
|
||||||
import 'package:markdown_widget/markdown_widget.dart';
|
import 'package:markdown_widget/markdown_widget.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import 'image.dart';
|
import 'image.dart';
|
||||||
@@ -23,6 +27,7 @@ class MarkdownTextContent extends HookConsumerWidget {
|
|||||||
final TextStyle? linkStyle;
|
final TextStyle? linkStyle;
|
||||||
final EdgeInsets? linesMargin;
|
final EdgeInsets? linesMargin;
|
||||||
final bool isSelectable;
|
final bool isSelectable;
|
||||||
|
final List<SnCloudFile>? attachments;
|
||||||
|
|
||||||
const MarkdownTextContent({
|
const MarkdownTextContent({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -33,6 +38,7 @@ class MarkdownTextContent extends HookConsumerWidget {
|
|||||||
this.linkStyle,
|
this.linkStyle,
|
||||||
this.isSelectable = false,
|
this.isSelectable = false,
|
||||||
this.linesMargin,
|
this.linesMargin,
|
||||||
|
this.attachments,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -109,6 +115,29 @@ class MarkdownTextContent extends HookConsumerWidget {
|
|||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
if (uri.scheme == 'solian') {
|
if (uri.scheme == 'solian') {
|
||||||
switch (uri.host) {
|
switch (uri.host) {
|
||||||
|
case 'files':
|
||||||
|
final file = attachments?.firstWhereOrNull(
|
||||||
|
(file) => file.id == uri.pathSegments[0],
|
||||||
|
);
|
||||||
|
if (file == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CloudFileWidget(
|
||||||
|
item: file,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
).clipRRect(all: 8),
|
||||||
|
),
|
||||||
|
);
|
||||||
case 'stickers':
|
case 'stickers':
|
||||||
final size = doesEnlargeSticker ? 96.0 : 24.0;
|
final size = doesEnlargeSticker ? 96.0 : 24.0;
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
@@ -132,9 +161,9 @@ class MarkdownTextContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final content = UniversalImage(
|
final content = ConstrainedBox(
|
||||||
uri: uri.toString(),
|
constraints: BoxConstraints(maxHeight: 360),
|
||||||
fit: BoxFit.cover,
|
child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain),
|
||||||
);
|
);
|
||||||
return content;
|
return content;
|
||||||
},
|
},
|
||||||
|
@@ -474,6 +474,23 @@ class ComposeLogic {
|
|||||||
state.attachments.value = clone;
|
state.attachments.value = clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void insertAttachment(WidgetRef ref, ComposeState state, int index) {
|
||||||
|
final attachment = state.attachments.value[index];
|
||||||
|
if (!attachment.isOnCloud) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final cloudFile = attachment.data as SnCloudFile;
|
||||||
|
final markdown = '';
|
||||||
|
final controller = state.contentController;
|
||||||
|
final text = controller.text;
|
||||||
|
final selection = controller.selection;
|
||||||
|
final newText = text.replaceRange(selection.start, selection.end, markdown);
|
||||||
|
controller.text = newText;
|
||||||
|
controller.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: selection.start + markdown.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> performAction(
|
static Future<void> performAction(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
ComposeState state,
|
ComposeState state,
|
||||||
|
@@ -56,7 +56,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
|
|
||||||
final user = ref.watch(userInfoProvider);
|
final user = ref.watch(userInfoProvider);
|
||||||
final isAuthor = useMemoized(
|
final isAuthor = useMemoized(
|
||||||
() => user.hasValue && user.value?.id == item.publisher.accountId,
|
() => user.value != null && user.value?.id == item.publisher.accountId,
|
||||||
[user],
|
[user],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
if ((item.repliedPost != null || item.forwardedPost != null) &&
|
if ((item.repliedPost != null || item.forwardedPost != null) &&
|
||||||
showReferencePost)
|
showReferencePost)
|
||||||
_buildReferencePost(context, item),
|
_buildReferencePost(context, item),
|
||||||
if (item.attachments.isNotEmpty)
|
if (item.attachments.isNotEmpty && item.type != 1)
|
||||||
CloudFileList(
|
CloudFileList(
|
||||||
files: item.attachments,
|
files: item.attachments,
|
||||||
maxWidth: math.min(
|
maxWidth: math.min(
|
||||||
@@ -331,6 +331,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
item.type == 0
|
item.type == 0
|
||||||
? EdgeInsets.only(bottom: 8)
|
? EdgeInsets.only(bottom: 8)
|
||||||
: null,
|
: null,
|
||||||
|
attachments: item.attachments,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// Render tags and categories if they exist
|
// Render tags and categories if they exist
|
||||||
@@ -389,7 +390,7 @@ class PostItem extends HookConsumerWidget {
|
|||||||
item.forwardedPost != null) &&
|
item.forwardedPost != null) &&
|
||||||
showReferencePost)
|
showReferencePost)
|
||||||
_buildReferencePost(context, item),
|
_buildReferencePost(context, item),
|
||||||
if (item.attachments.isNotEmpty)
|
if (item.attachments.isNotEmpty && item.type != 1)
|
||||||
CloudFileList(
|
CloudFileList(
|
||||||
files: item.attachments,
|
files: item.attachments,
|
||||||
maxWidth: math.min(
|
maxWidth: math.min(
|
||||||
@@ -689,6 +690,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
|
|||||||
referencePost.type == 0
|
referencePost.type == 0
|
||||||
? EdgeInsets.only(bottom: 4)
|
? EdgeInsets.only(bottom: 4)
|
||||||
: null,
|
: null,
|
||||||
|
attachments: item.attachments,
|
||||||
).padding(bottom: 4),
|
).padding(bottom: 4),
|
||||||
// Truncation hint for referenced post
|
// Truncation hint for referenced post
|
||||||
if (referencePost.isTruncated)
|
if (referencePost.isTruncated)
|
||||||
@@ -696,7 +698,8 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
|
|||||||
isCompact: true,
|
isCompact: true,
|
||||||
margin: const EdgeInsets.only(top: 4, bottom: 8),
|
margin: const EdgeInsets.only(top: 4, bottom: 8),
|
||||||
),
|
),
|
||||||
if (referencePost.attachments.isNotEmpty)
|
if (referencePost.attachments.isNotEmpty &&
|
||||||
|
referencePost.type != 1)
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -1030,6 +1033,7 @@ class _ArticlePostDisplay extends StatelessWidget {
|
|||||||
MarkdownTextContent(
|
MarkdownTextContent(
|
||||||
content: item.content!,
|
content: item.content!,
|
||||||
textStyle: Theme.of(context).textTheme.bodyLarge,
|
textStyle: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
attachments: item.attachments,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/post/post_replies.dart';
|
import 'package:island/widgets/post/post_replies.dart';
|
||||||
import 'package:island/widgets/post/post_quick_reply.dart';
|
import 'package:island/widgets/post/post_quick_reply.dart';
|
||||||
@@ -14,6 +15,8 @@ class PostRepliesSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final user = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: 'repliesCount'.plural(post.repliesCount),
|
titleText: 'repliesCount'.plural(post.repliesCount),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -21,26 +24,29 @@ class PostRepliesSheet extends HookConsumerWidget {
|
|||||||
// Replies list
|
// Replies list
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [PostRepliesList(
|
slivers: [
|
||||||
postId: post.id.toString(),
|
PostRepliesList(
|
||||||
backgroundColor: Colors.transparent,
|
postId: post.id.toString(),
|
||||||
)],
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Quick reply section
|
// Quick reply section
|
||||||
Material(
|
if (user.value != null)
|
||||||
elevation: 2,
|
Material(
|
||||||
child: PostQuickReply(
|
elevation: 2,
|
||||||
parent: post,
|
child: PostQuickReply(
|
||||||
onPosted: () {
|
parent: post,
|
||||||
ref.invalidate(postRepliesNotifierProvider(post.id));
|
onPosted: () {
|
||||||
},
|
ref.invalidate(postRepliesNotifierProvider(post.id));
|
||||||
).padding(
|
},
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
).padding(
|
||||||
top: 16,
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
horizontal: 16,
|
top: 16,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.0.0+110
|
version: 3.0.0+111
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
|
Reference in New Issue
Block a user