Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e544c0b6c | |||
7d56c5ef31 | |||
c2df1af16d | |||
a8143c6453 | |||
04065061e0 | |||
226eb452e5 | |||
a6715b0872 | |||
43e3404dbb | |||
c91cf7c813 | |||
42ac12b53e | |||
63567bf708 | |||
5d3cadefef | |||
251fbb2503 | |||
0b31d32217 | |||
5ddd4fed2e | |||
48b6d5f6c1 |
23
.github/workflows/nightly.yml
vendored
23
.github/workflows/nightly.yml
vendored
@ -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
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ body:json {
|
|||||||
"client_id": "alphabot",
|
"client_id": "alphabot",
|
||||||
"client_secret": "_uR0sVnHTh",
|
"client_secret": "_uR0sVnHTh",
|
||||||
"remark": "新年红包",
|
"remark": "新年红包",
|
||||||
"amount": 9705,
|
"amount": 150,
|
||||||
"payee_id": 2
|
"payee_id": 18
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -638,5 +638,17 @@
|
|||||||
"pollVotes": {
|
"pollVotes": {
|
||||||
"one": "{} vote",
|
"one": "{} vote",
|
||||||
"other": "{} votes"
|
"other": "{} votes"
|
||||||
}
|
},
|
||||||
|
"publisherDelete": "Delete Publisher {}",
|
||||||
|
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
|
||||||
|
"channelIsPublic": "Public Channel",
|
||||||
|
"channelIsPublicDescription": "The channel is public, anyone can join.",
|
||||||
|
"channelIsCommunity": "Community Channel",
|
||||||
|
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
|
||||||
|
"realmIsPublic": "Public Realm",
|
||||||
|
"realmIsPublicDescription": "The realm is public, anyone can join.",
|
||||||
|
"realmIsCommunity": "Community Realm",
|
||||||
|
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
|
||||||
|
"realmLeave": "Leave Realm",
|
||||||
|
"realmLeaveDescription": "Leave the current realm and delete the realm's identity."
|
||||||
}
|
}
|
||||||
|
@ -637,5 +637,17 @@
|
|||||||
"pollVotes": {
|
"pollVotes": {
|
||||||
"one": "{} 票",
|
"one": "{} 票",
|
||||||
"other": "{} 票"
|
"other": "{} 票"
|
||||||
}
|
},
|
||||||
|
"publisherDelete": "删除发布者 {}",
|
||||||
|
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
|
||||||
|
"channelIsPublic": "公开频道",
|
||||||
|
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
|
||||||
|
"channelIsCommunity": "社区频道",
|
||||||
|
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
|
||||||
|
"realmIsPublic": "公开领域",
|
||||||
|
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
|
||||||
|
"realmIsCommunity": "社区领域",
|
||||||
|
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
|
||||||
|
"realmLeave": "离开领域",
|
||||||
|
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。"
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -45,6 +45,33 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _deletePublisher(SnPublisher publisher) async {
|
||||||
|
final confirm = await context.showConfirmDialog(
|
||||||
|
'publisherDelete'.tr(args: ['#${publisher.name}']),
|
||||||
|
'publisherDeleteDescription'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await context
|
||||||
|
.read<SnNetworkProvider>()
|
||||||
|
.client
|
||||||
|
.delete('/cgi/co/publishers/${publisher.name}');
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showSnackbar('publisherDeleted'.tr(args: ['#${publisher.name}']));
|
||||||
|
_publishers.remove(publisher);
|
||||||
|
_fetchPublishers();
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -118,6 +145,18 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_deletePublisher(publisher);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -37,6 +37,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
|
|
||||||
SnChannel? _editingChannel;
|
SnChannel? _editingChannel;
|
||||||
|
|
||||||
|
bool _isPublic = false;
|
||||||
|
bool _isCommunity = false;
|
||||||
|
|
||||||
Future<void> _fetchRealms() async {
|
Future<void> _fetchRealms() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
try {
|
try {
|
||||||
@ -67,6 +70,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
_aliasController.text = _editingChannel!.alias;
|
_aliasController.text = _editingChannel!.alias;
|
||||||
_nameController.text = _editingChannel!.name;
|
_nameController.text = _editingChannel!.name;
|
||||||
_descriptionController.text = _editingChannel!.description;
|
_descriptionController.text = _editingChannel!.description;
|
||||||
|
_isPublic = _editingChannel!.isPublic;
|
||||||
|
_isCommunity = _editingChannel!.isCommunity;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -88,6 +93,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
: uuid.v4().replaceAll('-', '').substring(0, 12),
|
: uuid.v4().replaceAll('-', '').substring(0, 12),
|
||||||
'name': _nameController.text,
|
'name': _nameController.text,
|
||||||
'description': _descriptionController.text,
|
'description': _descriptionController.text,
|
||||||
|
'is_public': _isPublic,
|
||||||
|
'is_community': _isCommunity,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -271,6 +278,23 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
|||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _isPublic,
|
||||||
|
title: Text('channelIsPublic'.tr()),
|
||||||
|
subtitle: Text('channelIsPublicDescription'.tr()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _isPublic = value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _isCommunity,
|
||||||
|
title: Text('channelIsCommunity'.tr()),
|
||||||
|
subtitle: Text('channelIsCommunityDescription'.tr()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _isCommunity = value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
@ -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: [
|
||||||
|
@ -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';
|
||||||
@ -110,7 +111,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,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -136,6 +137,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
builder: (context) => _PostPublisherPopup(
|
builder: (context) => _PostPublisherPopup(
|
||||||
controller: _writeController,
|
controller: _writeController,
|
||||||
publishers: _publishers,
|
publishers: _publishers,
|
||||||
|
onUpdate: () {
|
||||||
|
_fetchPublishers();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -160,7 +164,9 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +256,62 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (_writeController.replyingPost != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.reply, size: 16),
|
||||||
|
const Gap(10),
|
||||||
|
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
|
||||||
|
const Gap(4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_writeController.replyingPost!.body['content'],
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_writeController.repostingPost != null)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.forward, size: 16),
|
||||||
|
const Gap(10),
|
||||||
|
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
|
||||||
|
const Gap(4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_writeController.repostingPost!.body['content'],
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
@ -434,8 +496,9 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
|
|||||||
class _PostPublisherPopup extends StatelessWidget {
|
class _PostPublisherPopup extends StatelessWidget {
|
||||||
final PostWriteController controller;
|
final PostWriteController controller;
|
||||||
final List<SnPublisher>? publishers;
|
final List<SnPublisher>? publishers;
|
||||||
|
final Function onUpdate;
|
||||||
|
|
||||||
const _PostPublisherPopup({required this.controller, this.publishers});
|
const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -450,6 +513,20 @@ class _PostPublisherPopup extends StatelessWidget {
|
|||||||
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').then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
onUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: publishers?.length ?? 0,
|
itemCount: publishers?.length ?? 0,
|
||||||
|
@ -50,6 +50,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
_aliasController.text = out.alias;
|
_aliasController.text = out.alias;
|
||||||
_nameController.text = out.name;
|
_nameController.text = out.name;
|
||||||
_descriptionController.text = out.description;
|
_descriptionController.text = out.description;
|
||||||
|
_isPublic = out.isPublic;
|
||||||
|
_isCommunity = out.isCommunity;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
if (context.mounted) context.showErrorDialog(err);
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
@ -67,6 +69,9 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
|
|
||||||
|
bool _isPublic = false;
|
||||||
|
bool _isCommunity = false;
|
||||||
|
|
||||||
Future<void> _updateImage(String place) async {
|
Future<void> _updateImage(String place) async {
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
@ -138,6 +143,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
'description': _descriptionController.text,
|
'description': _descriptionController.text,
|
||||||
'avatar': _avatar,
|
'avatar': _avatar,
|
||||||
'banner': _banner,
|
'banner': _banner,
|
||||||
|
'is_public': _isPublic,
|
||||||
|
'is_community': _isCommunity,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -293,6 +300,23 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
|||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _isPublic,
|
||||||
|
title: Text('realmIsPublic'.tr()),
|
||||||
|
subtitle: Text('realmIsPublicDescription'.tr()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _isPublic = value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: _isCommunity,
|
||||||
|
title: Text('realmIsCommunity'.tr()),
|
||||||
|
subtitle: Text('realmIsCommunityDescription'.tr()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _isCommunity = value ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
@ -357,6 +357,28 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _leaveRealm() async {
|
||||||
|
final confirm = await context.showConfirmDialog(
|
||||||
|
'realmLeave'.tr(),
|
||||||
|
'realmLeaveDescription'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirm) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me');
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
@ -366,6 +388,15 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.logout),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
title: Text('realmLeave').tr(),
|
||||||
|
subtitle: Text('realmLeaveDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
onTap: _isBusy ? null : () => _leaveRealm(),
|
||||||
|
),
|
||||||
|
if (isOwned)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.edit),
|
leading: const Icon(Symbols.edit),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
@ -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>(),
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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 {},
|
||||||
);
|
);
|
||||||
|
@ -45,7 +45,12 @@ 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,
|
||||||
|
);
|
||||||
|
final HotKey _newLineHotKey = HotKey(
|
||||||
|
key: PhysicalKeyboardKey.enter,
|
||||||
|
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -61,6 +66,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
));
|
));
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
hotKeyManager.register(_newLineHotKey, keyDownHandler: (_) async {
|
||||||
|
if (_contentController.text.isEmpty) return;
|
||||||
|
_contentController.text += '\n';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -112,6 +121,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>();
|
||||||
@ -204,7 +214,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
_contentController.dispose();
|
_contentController.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
_dismissEmojiPicker();
|
_dismissEmojiPicker();
|
||||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||||
|
hotKeyManager.unregister(_pasteHotKey);
|
||||||
|
hotKeyManager.unregister(_newLineHotKey);
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,6 +357,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
_sendMessage();
|
_sendMessage();
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
},
|
},
|
||||||
|
maxLines: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
|
@ -965,7 +965,7 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {'mode': data.typePlural},
|
pathParameters: {'mode': 'stories'},
|
||||||
queryParameters: {'replying': data.id.toString()},
|
queryParameters: {'replying': data.id.toString()},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -981,7 +981,7 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {'mode': data.typePlural},
|
pathParameters: {'mode': 'stories'},
|
||||||
queryParameters: {'reposting': data.id.toString()},
|
queryParameters: {'reposting': data.id.toString()},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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+69
|
||||||
|
|
||||||
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:
|
||||||
|
Reference in New Issue
Block a user