Compare commits

...

8 Commits

Author SHA1 Message Date
LittleSheep
c91cf7c813 🐛 Fix send empty message 2025-02-15 18:12:35 +08:00
LittleSheep
42ac12b53e 🔨 Fix linux build script 2025-02-15 13:48:25 +08:00
LittleSheep
63567bf708 🔨 Fix linux build missing deps 2025-02-15 13:43:21 +08:00
LittleSheep
5d3cadefef 🔨 Add linux build pipeline 2025-02-15 13:39:00 +08:00
LittleSheep
251fbb2503 🐛 Try to fix github action build error 2025-02-15 13:34:23 +08:00
LittleSheep
0b31d32217 💄 Fix some designs issue
🐛 Fix web some pages error
2025-02-15 13:06:25 +08:00
LittleSheep
5ddd4fed2e 🐛 Fix missing new publisher button 2025-02-15 01:17:09 +08:00
LittleSheep
48b6d5f6c1 🐛 Fix poll 2025-02-15 00:16:06 +08:00
16 changed files with 215 additions and 95 deletions

View File

@@ -39,3 +39,26 @@ jobs:
with: with:
name: build-output-windows name: build-output-windows
path: build/windows/x64/runner/Release path: build/windows/x64/runner/Release
build-linux:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install libmpv-dev mpv
sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install keybinder-3.0
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux
path: build/linux/x64/release/bundle

View File

@@ -5,7 +5,7 @@ meta {
} }
post { post {
url: {{endpoint}}/cgi/id/dev/notify/1 url: {{endpoint}}/cgi/id/dev/notify/122
body: json body: json
auth: inherit auth: inherit
} }
@@ -15,12 +15,9 @@ body:json {
"client_id": "{{third_client_id}}", "client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}", "client_secret":"{{third_client_tk}}",
"type": "general", "type": "general",
"subject": "测试", "subject": "处理该帐号 @solian 的决定",
"subtitle": "Alphabot です", "subtitle": "违反用户协议",
"content": "全新通知动画", "content": "您的帐号违反了我们用户协议中关于冒充我们官方的行为,至此做出停权的决定。还请见谅。该决定是最终决定,不接受上诉。",
"metadata": {
"image": "D2EDbcrsTugs3xk5"
},
"priority": 10 "priority": 10
} }
} }

View File

@@ -47,6 +47,8 @@ import 'package:tray_manager/tray_manager.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart'; import 'package:in_app_review/in_app_review.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
void appBackgroundDispatcher() { void appBackgroundDispatcher() {
@@ -107,6 +109,13 @@ void main() async {
} }
} }
if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
}
runApp(const SolianApp()); runApp(const SolianApp());
} }
@@ -160,8 +169,8 @@ class SolianApp extends StatelessWidget {
), ),
), ),
breakpoints: [ breakpoints: [
const Breakpoint(start: 0, end: 450, name: MOBILE), const Breakpoint(start: 0, end: 600, name: MOBILE),
const Breakpoint(start: 451, end: 800, name: TABLET), const Breakpoint(start: 601, end: 800, name: TABLET),
const Breakpoint(start: 801, end: 1920, name: DESKTOP), const Breakpoint(start: 801, end: 1920, name: DESKTOP),
], ],
); );

View File

@@ -42,13 +42,14 @@ class WebSocketProvider extends ChangeNotifier {
_connectCompleter = null; _connectCompleter = null;
} }
_connectCompleter = Completer<void>();
if (!_ua.isAuthorized) return; if (!_ua.isAuthorized) return;
if (isConnected || conn != null) { if (isConnected || conn != null) {
disconnect(); disconnect();
} }
try {
_connectCompleter = Completer<void>();
final atk = await _sn.getFreshAtk(); final atk = await _sn.getFreshAtk();
final uri = Uri.parse( final uri = Uri.parse(
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk', '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
@@ -57,7 +58,6 @@ class WebSocketProvider extends ChangeNotifier {
isBusy = true; isBusy = true;
notifyListeners(); notifyListeners();
try {
conn = WebSocketChannel.connect(uri); conn = WebSocketChannel.connect(uri);
await conn!.ready; await conn!.ready;
_wsStream = conn!.stream.asBroadcastStream(); _wsStream = conn!.stream.asBroadcastStream();
@@ -82,6 +82,7 @@ class WebSocketProvider extends ChangeNotifier {
isBusy = false; isBusy = false;
notifyListeners(); notifyListeners();
_connectCompleter!.complete(); _connectCompleter!.complete();
_connectCompleter = null;
} }
} }

View File

@@ -74,7 +74,10 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
), ),
const Divider(height: 1), const Divider(height: 1),
if (_isBusy) if (_isBusy)
const CircularProgressIndicator().padding(all: 24).center() Padding(
padding: const EdgeInsets.all(24),
child: const CircularProgressIndicator(),
).center()
else else
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(

View File

@@ -123,8 +123,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
), ),
if (_isBusy) if (_isBusy)
SliverToBoxAdapter( SliverToBoxAdapter(
child: child: Padding(
const CircularProgressIndicator().padding(all: 24).center(), padding: const EdgeInsets.all(24),
child: const CircularProgressIndicator(),
).center(),
), ),
], ],
), ),

View File

@@ -131,6 +131,7 @@ class _HomeDashUpdateWidget extends StatelessWidget {
return Container( return Container(
padding: padding, padding: padding,
child: Card( child: Card(
margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
leading: Icon(Symbols.update), leading: Icon(Symbols.update),
title: Text('updateAvailable').tr(), title: Text('updateAvailable').tr(),
@@ -180,6 +181,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
return Column( return Column(
children: days.map((ele) { children: days.map((ele) {
return Card( return Card(
margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24), leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']), title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
@@ -203,6 +205,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
final progress = dayz.getSpecialDayProgress(lastOne.$2, date); final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
final diff = nextOne.$2.difference(DateTime.now()); final diff = nextOne.$2.difference(DateTime.now());
return Card( return Card(
margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]), title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
@@ -270,6 +273,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -469,6 +473,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -594,6 +599,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -667,11 +673,13 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy) { if (_isBusy) {
return Card( return Card(
margin: EdgeInsets.zero,
child: CircularProgressIndicator().center(), child: CircularProgressIndicator().center(),
); );
} }
return Card( return Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
@@ -90,8 +91,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
final beforeId = config.prefs.getInt('int_last_publisher_id'); final beforeId = config.prefs.getInt('int_last_publisher_id');
_writeController _writeController.setPublisher(
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull); _publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
_publishers?.firstOrNull);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -110,7 +112,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
final HotKey _pasteHotKey = HotKey( final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV, key: PhysicalKeyboardKey.keyV,
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
@@ -160,7 +162,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
@override @override
void dispose() { void dispose() {
_writeController.dispose(); _writeController.dispose();
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS))
hotKeyManager.unregister(_pasteHotKey);
super.dispose(); super.dispose();
} }
@@ -184,7 +187,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
if (widget.extraProps != null) { if (widget.extraProps != null) {
_writeController.contentController.text = widget.extraProps!.text ?? ''; _writeController.contentController.text = widget.extraProps!.text ?? '';
_writeController.titleController.text = widget.extraProps!.title ?? ''; _writeController.titleController.text = widget.extraProps!.title ?? '';
_writeController.descriptionController.text = widget.extraProps!.description ?? ''; _writeController.descriptionController.text =
widget.extraProps!.description ?? '';
_writeController.addAttachments(widget.extraProps!.attachments ?? []); _writeController.addAttachments(widget.extraProps!.attachments ?? []);
} }
} }
@@ -205,7 +209,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _writeController.title.isNotEmpty ? _writeController.title : 'untitled'.tr(), text: _writeController.title.isNotEmpty
? _writeController.title
: 'untitled'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!, color: Theme.of(context).appBarTheme.foregroundColor!,
), ),
@@ -232,7 +238,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [ children: [
if (_writeController.editingPost != null) if (_writeController.editingPost != null)
Container( Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20), padding: const EdgeInsets.only(
top: 4, bottom: 4, left: 20, right: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
@@ -246,7 +253,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [ children: [
const Icon(Icons.edit, size: 16), const Icon(Icons.edit, size: 16),
const Gap(10), const Gap(10),
Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']), Text('postEditingNotice').tr(args: [
'@${_writeController.editingPost!.publisher.name}'
]),
], ],
), ),
), ),
@@ -276,7 +285,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}) })
.padding(top: 8), .padding(top: 8),
), ),
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) if (_writeController.attachments.isNotEmpty ||
_writeController.thumbnail != null)
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
@@ -286,7 +296,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
attachments: _writeController.attachments, attachments: _writeController.attachments,
isBusy: _writeController.isBusy, isBusy: _writeController.isBusy,
onUpload: (int idx) async { onUpload: (int idx) async {
await _writeController.uploadSingleAttachment(context, idx); await _writeController.uploadSingleAttachment(
context, idx);
}, },
onPostSetThumbnail: (int? idx) { onPostSetThumbnail: (int? idx) {
_writeController.setThumbnail(idx); _writeController.setThumbnail(idx);
@@ -295,10 +306,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
_writeController.contentController.text += _writeController.contentController.text +=
'\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})'; '\n![](solink://attachments/${_writeController.attachments[idx].attachment!.rid})';
}, },
onUpdate: (int idx, PostWriteMedia updatedMedia) async { onUpdate:
(int idx, PostWriteMedia updatedMedia) async {
_writeController.setIsBusy(true); _writeController.setIsBusy(true);
try { try {
_writeController.setAttachmentAt(idx, updatedMedia); _writeController.setAttachmentAt(
idx, updatedMedia);
} finally { } finally {
_writeController.setIsBusy(false); _writeController.setIsBusy(false);
} }
@@ -311,7 +324,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
_writeController.setIsBusy(false); _writeController.setIsBusy(false);
} }
}, },
onUpdateBusy: (state) => _writeController.setIsBusy(state), onUpdateBusy: (state) =>
_writeController.setIsBusy(state),
).padding(bottom: 8), ).padding(bottom: 8),
), ),
], ],
@@ -322,11 +336,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_writeController.isBusy && _writeController.progress != null) if (_writeController.isBusy &&
_writeController.progress != null)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress), tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2),
) )
else if (_writeController.isBusy) else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2), const LinearProgressIndicator(value: null, minHeight: 2),
@@ -335,12 +351,14 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
Container( Container(
child: _writeController.temporaryRestored child: _writeController.temporaryRestored
? Container( ? Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22), padding: const EdgeInsets.only(
top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio, width: 1 /
MediaQuery.of(context).devicePixelRatio,
), ),
), ),
), ),
@@ -349,7 +367,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
children: [ children: [
const Icon(Icons.restore, size: 20), const Icon(Icons.restore, size: 20),
const Gap(8), const Gap(8),
Expanded(child: Text('postLocalDraftRestored').tr()), Expanded(
child:
Text('postLocalDraftRestored').tr()),
InkWell( InkWell(
child: Text('dialogDismiss').tr(), child: Text('dialogDismiss').tr(),
onTap: () { onTap: () {
@@ -360,8 +380,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
)) ))
: const SizedBox.shrink(), : const SizedBox.shrink(),
) )
.height(_writeController.temporaryRestored ? 32 : 0, animate: true) .height(_writeController.temporaryRestored ? 32 : 0,
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn), animate: true)
.animate(const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -381,11 +403,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
if (_writeController.mode == 'stories') if (_writeController.mode == 'stories')
IconButton( IconButton(
icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary), icon: Icon(Symbols.poll,
color: Theme.of(context)
.colorScheme
.primary),
style: ButtonStyle( style: ButtonStyle(
backgroundColor: _writeController.poll == null backgroundColor:
_writeController.poll == null
? null ? null
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer), : WidgetStatePropertyAll(
Theme.of(context)
.colorScheme
.surfaceContainer),
), ),
onPressed: () { onPressed: () {
_showPollEditorDialog(); _showPollEditorDialog();
@@ -397,7 +426,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
), ),
TextButton.icon( TextButton.icon(
onPressed: (_writeController.isBusy || _writeController.publisher == null) onPressed: (_writeController.isBusy ||
_writeController.publisher == null)
? null ? null
: () { : () {
_writeController.sendPost(context).then((_) { _writeController.sendPost(context).then((_) {
@@ -447,9 +477,21 @@ class _PostPublisherPopup extends StatelessWidget {
children: [ children: [
const Icon(Symbols.face, size: 24), const Icon(Symbols.face, size: 24),
const Gap(16), const Gap(16),
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(), Text('accountPublishers',
style: Theme.of(context).textTheme.titleLarge)
.tr(),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
ListTile(
leading: const Icon(Symbols.add),
title: Text('publishersNew').tr(),
subtitle: Text('publisherNewSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () {
GoRouter.of(context).pushNamed('accountPublisherNew');
},
),
const Divider(height: 1),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: publishers?.length ?? 0, itemCount: publishers?.length ?? 0,
@@ -509,7 +551,8 @@ class _PostStoryEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(8), const Gap(8),
TextField( TextField(
@@ -524,7 +567,8 @@ class _PostStoryEditor extends StatelessWidget {
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
@@ -614,7 +658,8 @@ class _PostArticleEditor extends StatelessWidget {
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@@ -693,7 +738,8 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
), ),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(8), const Gap(8),
TextField( TextField(
@@ -704,7 +750,8 @@ class _PostQuestionEditor extends StatelessWidget {
border: InputBorder.none, border: InputBorder.none,
isCollapsed: true, isCollapsed: true,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16), ).padding(horizontal: 16),
const Gap(8), const Gap(8),
TextField( TextField(
@@ -719,7 +766,8 @@ class _PostQuestionEditor extends StatelessWidget {
), ),
border: InputBorder.none, border: InputBorder.none,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
@@ -755,7 +803,8 @@ class _PostVideoEditor extends StatelessWidget {
final result = await showDialog<SnAttachment?>( final result = await showDialog<SnAttachment?>(
context: context, context: context,
builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)), builder: (context) => PendingAttachmentAltDialog(
media: PostWriteMedia(controller.videoAttachment)),
); );
if (result == null) return; if (result == null) return;
@@ -767,7 +816,8 @@ class _PostVideoEditor extends StatelessWidget {
final result = await showDialog<SnAttachmentBoost?>( final result = await showDialog<SnAttachmentBoost?>(
context: context, context: context,
builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)), builder: (context) => PendingAttachmentBoostDialog(
media: PostWriteMedia(controller.videoAttachment)),
); );
if (result == null) return; if (result == null) return;
@@ -810,7 +860,8 @@ class _PostVideoEditor extends StatelessWidget {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}'); await sn.client
.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
controller.setVideoAttachment(null); controller.setVideoAttachment(null);
} catch (err) { } catch (err) {
if (!context.mounted) return; if (!context.mounted) return;
@@ -902,7 +953,8 @@ class _PostVideoEditor extends StatelessWidget {
label: 'attachmentCopyRandomId'.tr(), label: 'attachmentCopyRandomId'.tr(),
icon: Symbols.content_copy, icon: Symbols.content_copy,
onSelected: () { onSelected: () {
Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid)); Clipboard.setData(
ClipboardData(text: controller.videoAttachment!.rid));
}, },
), ),
MenuItem( MenuItem(
@@ -921,7 +973,9 @@ class _PostVideoEditor extends StatelessWidget {
), ),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null, onTap: controller.videoAttachment == null
? () => _selectVideo(context)
: null,
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: controller.videoAttachment == null child: controller.videoAttachment == null

View File

@@ -154,7 +154,7 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}'); final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
final out = List<SnChannel>.from( final out = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(), resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
); );

View File

@@ -16,8 +16,7 @@ class SnPoll with _$SnPoll {
required SnPollMetric metric, required SnPollMetric metric,
}) = _SnPoll; }) = _SnPoll;
factory SnPoll.fromJson(Map<String, Object?> json) => factory SnPoll.fromJson(Map<String, Object?> json) => _$SnPollFromJson(json);
_$SnPollFromJson(json);
} }
@freezed @freezed
@@ -25,11 +24,11 @@ class SnPollMetric with _$SnPollMetric {
const factory SnPollMetric({ const factory SnPollMetric({
required int totalAnswer, required int totalAnswer,
@Default({}) Map<String, int> byOptions, @Default({}) Map<String, int> byOptions,
@Default({}) Map<String, int> byOptionsPercentage, @Default({}) Map<String, double> byOptionsPercentage,
}) = _SnPollMetric; }) = _SnPollMetric;
factory SnPollMetric.fromJson(Map<String, Object?> json) factory SnPollMetric.fromJson(Map<String, Object?> json) =>
=> _$SnPollMetricFromJson(json); _$SnPollMetricFromJson(json);
} }
@freezed @freezed
@@ -41,6 +40,6 @@ class SnPollOption with _$SnPollOption {
required String description, required String description,
}) = _SnPollOption; }) = _SnPollOption;
factory SnPollOption.fromJson(Map<String, Object?> json) factory SnPollOption.fromJson(Map<String, Object?> json) =>
=> _$SnPollOptionFromJson(json); _$SnPollOptionFromJson(json);
} }

View File

@@ -345,7 +345,7 @@ SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) {
mixin _$SnPollMetric { mixin _$SnPollMetric {
int get totalAnswer => throw _privateConstructorUsedError; int get totalAnswer => throw _privateConstructorUsedError;
Map<String, int> get byOptions => throw _privateConstructorUsedError; Map<String, int> get byOptions => throw _privateConstructorUsedError;
Map<String, int> get byOptionsPercentage => Map<String, double> get byOptionsPercentage =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
/// Serializes this SnPollMetric to a JSON map. /// Serializes this SnPollMetric to a JSON map.
@@ -367,7 +367,7 @@ abstract class $SnPollMetricCopyWith<$Res> {
$Res call( $Res call(
{int totalAnswer, {int totalAnswer,
Map<String, int> byOptions, Map<String, int> byOptions,
Map<String, int> byOptionsPercentage}); Map<String, double> byOptionsPercentage});
} }
/// @nodoc /// @nodoc
@@ -401,7 +401,7 @@ class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric>
byOptionsPercentage: null == byOptionsPercentage byOptionsPercentage: null == byOptionsPercentage
? _value.byOptionsPercentage ? _value.byOptionsPercentage
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable : byOptionsPercentage // ignore: cast_nullable_to_non_nullable
as Map<String, int>, as Map<String, double>,
) as $Val); ) as $Val);
} }
} }
@@ -417,7 +417,7 @@ abstract class _$$SnPollMetricImplCopyWith<$Res>
$Res call( $Res call(
{int totalAnswer, {int totalAnswer,
Map<String, int> byOptions, Map<String, int> byOptions,
Map<String, int> byOptionsPercentage}); Map<String, double> byOptionsPercentage});
} }
/// @nodoc /// @nodoc
@@ -449,7 +449,7 @@ class __$$SnPollMetricImplCopyWithImpl<$Res>
byOptionsPercentage: null == byOptionsPercentage byOptionsPercentage: null == byOptionsPercentage
? _value._byOptionsPercentage ? _value._byOptionsPercentage
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable : byOptionsPercentage // ignore: cast_nullable_to_non_nullable
as Map<String, int>, as Map<String, double>,
)); ));
} }
} }
@@ -460,7 +460,7 @@ class _$SnPollMetricImpl implements _SnPollMetric {
const _$SnPollMetricImpl( const _$SnPollMetricImpl(
{required this.totalAnswer, {required this.totalAnswer,
final Map<String, int> byOptions = const {}, final Map<String, int> byOptions = const {},
final Map<String, int> byOptionsPercentage = const {}}) final Map<String, double> byOptionsPercentage = const {}})
: _byOptions = byOptions, : _byOptions = byOptions,
_byOptionsPercentage = byOptionsPercentage; _byOptionsPercentage = byOptionsPercentage;
@@ -478,10 +478,10 @@ class _$SnPollMetricImpl implements _SnPollMetric {
return EqualUnmodifiableMapView(_byOptions); return EqualUnmodifiableMapView(_byOptions);
} }
final Map<String, int> _byOptionsPercentage; final Map<String, double> _byOptionsPercentage;
@override @override
@JsonKey() @JsonKey()
Map<String, int> get byOptionsPercentage { Map<String, double> get byOptionsPercentage {
if (_byOptionsPercentage is EqualUnmodifiableMapView) if (_byOptionsPercentage is EqualUnmodifiableMapView)
return _byOptionsPercentage; return _byOptionsPercentage;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
@@ -534,7 +534,7 @@ abstract class _SnPollMetric implements SnPollMetric {
const factory _SnPollMetric( const factory _SnPollMetric(
{required final int totalAnswer, {required final int totalAnswer,
final Map<String, int> byOptions, final Map<String, int> byOptions,
final Map<String, int> byOptionsPercentage}) = _$SnPollMetricImpl; final Map<String, double> byOptionsPercentage}) = _$SnPollMetricImpl;
factory _SnPollMetric.fromJson(Map<String, dynamic> json) = factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
_$SnPollMetricImpl.fromJson; _$SnPollMetricImpl.fromJson;
@@ -544,7 +544,7 @@ abstract class _SnPollMetric implements SnPollMetric {
@override @override
Map<String, int> get byOptions; Map<String, int> get byOptions;
@override @override
Map<String, int> get byOptionsPercentage; Map<String, double> get byOptionsPercentage;
/// Create a copy of SnPollMetric /// Create a copy of SnPollMetric
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@@ -40,7 +40,7 @@ _$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
const {}, const {},
byOptionsPercentage: byOptionsPercentage:
(json['by_options_percentage'] as Map<String, dynamic>?)?.map( (json['by_options_percentage'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()), (k, e) => MapEntry(k, (e as num).toDouble()),
) ?? ) ??
const {}, const {},
); );

View File

@@ -45,7 +45,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final HotKey _pasteHotKey = HotKey( final HotKey _pasteHotKey = HotKey(
key: PhysicalKeyboardKey.keyV, key: PhysicalKeyboardKey.keyV,
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
scope: HotKeyScope.inapp, scope: HotKeyScope.inapp,
); );
@@ -112,6 +112,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
} }
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
if (_contentController.text.isEmpty && _attachments.isEmpty) return;
if (_isBusy) return; if (_isBusy) return;
final attach = context.read<SnAttachmentProvider>(); final attach = context.read<SnAttachmentProvider>();

View File

@@ -31,19 +31,27 @@ class _PostPollState extends State<PostPoll> {
String? _answeredChoice; String? _answeredChoice;
Future<void> _refreshPoll() async {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/polls/${widget.poll.id}');
if (!mounted) return;
setState(() => _poll = SnPoll.fromJson(resp.data!));
}
Future<void> _fetchAnswer() async { Future<void> _fetchAnswer() async {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) return; if (!ua.isAuthorized) return;
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer'); final resp =
await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer');
_answeredChoice = resp.data?['answer']; _answeredChoice = resp.data?['answer'];
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); // ignore because it may not found
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
@@ -59,8 +67,9 @@ class _PostPollState extends State<PostPoll> {
'answer': option.id, 'answer': option.id,
}); });
if (!mounted) return; if (!mounted) return;
context.showSnackbar('pollAnswered'.tr());
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
_answeredChoice = option.id;
_refreshPoll();
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@@ -78,15 +87,24 @@ class _PostPollState extends State<PostPoll> {
for (final option in _poll.options) for (final option in _poll.options)
Stack( Stack(
children: [ children: [
Container( ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
height: 60, height: 60,
width: MediaQuery.of(context).size.width * (_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble(), width: MediaQuery.of(context).size.width *
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
.toDouble(),
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
),
ListTile( ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
minTileHeight: 60, minTileHeight: 60,
leading: _answeredChoice == option.id ? const Icon(Symbols.circle, fill: 1) : const Icon(Symbols.circle), leading: _answeredChoice == option.id
? const Icon(Symbols.circle, fill: 1)
: const Icon(Symbols.circle),
title: Text(option.name), title: Text(option.name),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -95,14 +113,18 @@ class _PostPollState extends State<PostPoll> {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('pollVotes'.plural(_poll.metric.byOptions[option.id] ?? 0)), Text(
'pollVotes'
.plural(_poll.metric.byOptions[option.id] ?? 0),
),
Text(' · ').padding(horizontal: 4), Text(' · ').padding(horizontal: 4),
Text( Text(
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%', '${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
), ),
], ],
), ),
if (option.description.isNotEmpty) Text(option.description), if (option.description.isNotEmpty)
Text(option.description),
], ],
), ),
onTap: _isBusy ? null : () => _voteForOption(option), onTap: _isBusy ? null : () => _voteForOption(option),

View File

@@ -498,10 +498,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "3d57312a53746ed4eb8c843dc50372454bbda37dd0c01a4d40fedc83e2ce4921" sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.3.5" version: "8.3.7"
file_saver: file_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1043,7 +1043,7 @@ packages:
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
image_picker_android: image_picker_android:
dependency: transitive dependency: "direct main"
description: description:
name: image_picker_android name: image_picker_android
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 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: 2.3.2+67 version: 2.3.2+68
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@@ -120,6 +120,7 @@ dependencies:
xml: ^6.5.0 xml: ^6.5.0
tray_manager: ^0.3.2 tray_manager: ^0.3.2
hotkey_manager: ^0.2.3 hotkey_manager: ^0.2.3
image_picker_android: ^0.8.12+20
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: