Compare commits

...

5 Commits

Author SHA1 Message Date
e07da3efa5 Sliding window pricing of attachment billing info displaying 2025-02-20 21:19:23 +08:00
4f7f015250 ⬆️ I forgot what did I did last night 2025-02-20 20:41:41 +08:00
2a4c15d0dc 💄 Optimize About page 2025-02-20 20:41:25 +08:00
70ef894ec5 ♻️ Transferable chat channel 2025-02-18 23:34:59 +08:00
bb9179d5f9 🐛 Fix drawer remain when device rotate 2025-02-18 16:52:15 +08:00
16 changed files with 345 additions and 19 deletions

View File

@ -12,9 +12,9 @@ post {
body:json {
{
"alias": "BaLoading",
"name": "BaLoading",
"attachment_id": "2JCI2uh21mKkfk9P",
"pack_id": 3
"alias": "Deadge",
"name": "Dead",
"attachment_id": "pcbFd0u4zgdM39HM",
"pack_id": 4
}
}

View File

@ -5,7 +5,7 @@ meta {
}
post {
url: {{endpoint}}/cgi/id/dev/notify/122
url: {{endpoint}}/cgi/id/dev/notify/328
body: json
auth: inherit
}
@ -15,9 +15,9 @@ body:json {
"client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}",
"type": "general",
"subject": "处理该帐号 @solian 的决定",
"subtitle": "违反用户协议",
"content": "您的帐号违反了我们用户协议中关于冒充我们官方的行为,至此做出停权的决定。还请见谅。该决定是最终决定,不接受上诉。",
"subject": "处理该发布者 @vedal987 的决定",
"subtitle": "一条来自 Solar Network 客户支持的信息",
"content": "您的发布者违反了我们用户协议中的「禁止冒充他人」的相关条例,经管理决定,将相关内容隐藏。冒充他人的判定无论作者是否有主观意志,只要造成了误解我们就有责任处理。希望您能理解,本次决定未作出任何帐号相关的连带处罚。",
"priority": 10
}
}

View File

@ -548,6 +548,7 @@
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates.",
"unauthorized": "Unauthorized",
"unauthorizedDescription": "Login to explore the entire Solar Network.",
"projectDetail": "Project Details",
"serviceStatus": "Service Status",
"termRelated": "Related Terms",
"appDetails": "App Details",
@ -665,5 +666,9 @@
"zero": "No views",
"one": "{} view",
"other": "{} views"
}
},
"attachmentBillingUploaded": "Used space",
"attachmentBillingDiscount": "Free space",
"attachmentBillingRatio": "Usage",
"attachmentBillingHint": "Sliding Window Pricing®\nFees will only apply if the size of the file uploaded within 24 hours exceeds the free space."
}

View File

@ -546,6 +546,7 @@
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。",
"unauthorized": "未登陆",
"unauthorizedDescription": "登陆以探索整个 Solar Network。",
"projectDetail": "项目详情",
"serviceStatus": "服务状态",
"termRelated": "相关条款",
"appDetails": "应用程序详情",
@ -664,5 +665,8 @@
"zero": "{} 次浏览",
"one": "{} 次浏览",
"other": "{} 次浏览"
}
},
"attachmentBillingUploaded": "已占用的字节数",
"attachmentBillingDiscount": "免费的字节数",
"attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。"
}

View File

@ -546,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態",
"termRelated": "相關條款",
"appDetails": "應用程序詳情",
@ -664,5 +665,8 @@
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
}
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。"
}

View File

@ -546,6 +546,7 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態",
"termRelated": "相關條款",
"appDetails": "應用程序詳情",
@ -664,5 +665,8 @@
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
}
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。"
}

View File

@ -87,7 +87,7 @@ void main() async {
Hive.registerAdapter(SnChannelMemberImplAdapter());
Hive.registerAdapter(SnChatMessageImplAdapter());
if (kIsWeb && !Platform.isLinux) {
if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
@ -427,8 +427,16 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
});
return false;
},
child: SizeChangedLayoutNotifier(
child: widget.child,
child: OrientationBuilder(
builder: (context, orientation) {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return SizeChangedLayoutNotifier(
child: widget.child,
);
},
),
);
}

View File

@ -1,7 +1,13 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
@ -27,9 +33,23 @@ class _AlbumScreenState extends State<AlbumScreen> {
bool _isBusy = false;
int? _totalCount;
SnAttachmentBilling? _billing;
final List<SnAttachment> _attachments = List.empty(growable: true);
final List<String> _heroTags = List.empty(growable: true);
Future<void> _fetchBillingStatus() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/uc/billing');
final out = SnAttachmentBilling.fromJson(resp.data);
setState(() => _billing = out);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
}
}
Future<void> _fetchAttachments() async {
setState(() => _isBusy = true);
@ -62,6 +82,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
@override
void initState() {
super.initState();
_fetchBillingStatus();
_fetchAttachments();
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
@ -91,6 +112,48 @@ class _AlbumScreenState extends State<AlbumScreen> {
leading: AutoAppBarLeading(),
title: Text('screenAlbum').tr(),
),
SliverToBoxAdapter(
child: Card(
child: Row(
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(
value: _billing?.includedRatio ?? 0,
strokeWidth: 8,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
),
).padding(all: 12),
const Gap(24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachmentBillingUploaded').tr().bold(),
Text(
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
style: GoogleFonts.robotoMono(),
),
Text('attachmentBillingDiscount').tr().bold(),
Text(
'${(_billing?.discountFileSize ?? 0).formatBytes(decimals: 2)} · ${((_billing?.includedRatio ?? 0) * 100).toStringAsFixed(2)}%',
style: GoogleFonts.robotoMono(),
),
],
),
),
Tooltip(
message: 'attachmentBillingHint'.tr(),
child: IconButton(
icon: const Icon(Symbols.info),
onPressed: () {},
),
),
],
).padding(horizontal: 24, vertical: 8),
),
),
SliverMasonryGrid.extent(
childCount: _attachments.length,
maxCrossAxisExtent: 320,

View File

@ -95,6 +95,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
'description': _descriptionController.text,
'is_public': _isPublic,
'is_community': _isCommunity,
if (_editingChannel != null && _belongToRealm == null)
'new_belongs_realm': 'global'
else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id)
'new_belongs_realm': _belongToRealm!.alias,
};
try {
@ -171,7 +175,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
items: [
...(_realms?.map(
(SnRealm item) => DropdownMenuItem<SnRealm>(
enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
value: item,
child: Row(
children: [
@ -204,7 +207,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
) ??
[]),
DropdownMenuItem<SnRealm>(
enabled: _editingChannel == null,
value: null,
child: Row(
children: [

View File

@ -235,6 +235,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
),
Text(
widget.realm.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
],

View File

@ -177,3 +177,14 @@ class SnStickerPack with _$SnStickerPack {
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
}
@freezed
class SnAttachmentBilling with _$SnAttachmentBilling {
const factory SnAttachmentBilling({
required int currentBytes,
required int discountFileSize,
required double includedRatio,
}) = _SnAttachmentBilling;
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) => _$SnAttachmentBillingFromJson(json);
}

View File

@ -3007,3 +3007,195 @@ abstract class _SnStickerPack implements SnStickerPack {
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnAttachmentBilling _$SnAttachmentBillingFromJson(Map<String, dynamic> json) {
return _SnAttachmentBilling.fromJson(json);
}
/// @nodoc
mixin _$SnAttachmentBilling {
int get currentBytes => throw _privateConstructorUsedError;
int get discountFileSize => throw _privateConstructorUsedError;
double get includedRatio => throw _privateConstructorUsedError;
/// Serializes this SnAttachmentBilling to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnAttachmentBillingCopyWith<SnAttachmentBilling> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnAttachmentBillingCopyWith<$Res> {
factory $SnAttachmentBillingCopyWith(
SnAttachmentBilling value, $Res Function(SnAttachmentBilling) then) =
_$SnAttachmentBillingCopyWithImpl<$Res, SnAttachmentBilling>;
@useResult
$Res call({int currentBytes, int discountFileSize, double includedRatio});
}
/// @nodoc
class _$SnAttachmentBillingCopyWithImpl<$Res, $Val extends SnAttachmentBilling>
implements $SnAttachmentBillingCopyWith<$Res> {
_$SnAttachmentBillingCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentBytes = null,
Object? discountFileSize = null,
Object? includedRatio = null,
}) {
return _then(_value.copyWith(
currentBytes: null == currentBytes
? _value.currentBytes
: currentBytes // ignore: cast_nullable_to_non_nullable
as int,
discountFileSize: null == discountFileSize
? _value.discountFileSize
: discountFileSize // ignore: cast_nullable_to_non_nullable
as int,
includedRatio: null == includedRatio
? _value.includedRatio
: includedRatio // ignore: cast_nullable_to_non_nullable
as double,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnAttachmentBillingImplCopyWith<$Res>
implements $SnAttachmentBillingCopyWith<$Res> {
factory _$$SnAttachmentBillingImplCopyWith(_$SnAttachmentBillingImpl value,
$Res Function(_$SnAttachmentBillingImpl) then) =
__$$SnAttachmentBillingImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int currentBytes, int discountFileSize, double includedRatio});
}
/// @nodoc
class __$$SnAttachmentBillingImplCopyWithImpl<$Res>
extends _$SnAttachmentBillingCopyWithImpl<$Res, _$SnAttachmentBillingImpl>
implements _$$SnAttachmentBillingImplCopyWith<$Res> {
__$$SnAttachmentBillingImplCopyWithImpl(_$SnAttachmentBillingImpl _value,
$Res Function(_$SnAttachmentBillingImpl) _then)
: super(_value, _then);
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? currentBytes = null,
Object? discountFileSize = null,
Object? includedRatio = null,
}) {
return _then(_$SnAttachmentBillingImpl(
currentBytes: null == currentBytes
? _value.currentBytes
: currentBytes // ignore: cast_nullable_to_non_nullable
as int,
discountFileSize: null == discountFileSize
? _value.discountFileSize
: discountFileSize // ignore: cast_nullable_to_non_nullable
as int,
includedRatio: null == includedRatio
? _value.includedRatio
: includedRatio // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnAttachmentBillingImpl implements _SnAttachmentBilling {
const _$SnAttachmentBillingImpl(
{required this.currentBytes,
required this.discountFileSize,
required this.includedRatio});
factory _$SnAttachmentBillingImpl.fromJson(Map<String, dynamic> json) =>
_$$SnAttachmentBillingImplFromJson(json);
@override
final int currentBytes;
@override
final int discountFileSize;
@override
final double includedRatio;
@override
String toString() {
return 'SnAttachmentBilling(currentBytes: $currentBytes, discountFileSize: $discountFileSize, includedRatio: $includedRatio)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnAttachmentBillingImpl &&
(identical(other.currentBytes, currentBytes) ||
other.currentBytes == currentBytes) &&
(identical(other.discountFileSize, discountFileSize) ||
other.discountFileSize == discountFileSize) &&
(identical(other.includedRatio, includedRatio) ||
other.includedRatio == includedRatio));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, currentBytes, discountFileSize, includedRatio);
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnAttachmentBillingImplCopyWith<_$SnAttachmentBillingImpl> get copyWith =>
__$$SnAttachmentBillingImplCopyWithImpl<_$SnAttachmentBillingImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnAttachmentBillingImplToJson(
this,
);
}
}
abstract class _SnAttachmentBilling implements SnAttachmentBilling {
const factory _SnAttachmentBilling(
{required final int currentBytes,
required final int discountFileSize,
required final double includedRatio}) = _$SnAttachmentBillingImpl;
factory _SnAttachmentBilling.fromJson(Map<String, dynamic> json) =
_$SnAttachmentBillingImpl.fromJson;
@override
int get currentBytes;
@override
int get discountFileSize;
@override
double get includedRatio;
/// Create a copy of SnAttachmentBilling
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnAttachmentBillingImplCopyWith<_$SnAttachmentBillingImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -281,3 +281,19 @@ Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
'account_id': instance.accountId,
};
_$SnAttachmentBillingImpl _$$SnAttachmentBillingImplFromJson(
Map<String, dynamic> json) =>
_$SnAttachmentBillingImpl(
currentBytes: (json['current_bytes'] as num).toInt(),
discountFileSize: (json['discount_file_size'] as num).toInt(),
includedRatio: (json['included_ratio'] as num).toDouble(),
);
Map<String, dynamic> _$$SnAttachmentBillingImplToJson(
_$SnAttachmentBillingImpl instance) =>
<String, dynamic>{
'current_bytes': instance.currentBytes,
'discount_file_size': instance.discountFileSize,
'included_ratio': instance.includedRatio,
};

View File

@ -97,6 +97,13 @@ class AboutScreen extends StatelessWidget {
launchUrlString('https://status.solsynth.dev');
},
),
TextButton(
style: denseButtonStyle,
child: Text('projectDetail').tr(),
onPressed: () {
launchUrlString('https://solsynth.dev/products/solar-network');
},
),
],
),
).center(),
@ -108,6 +115,12 @@ class AboutScreen extends StatelessWidget {
fontSize: 12,
),
),
InkWell(
child: Text('GitHub', style: TextStyle(fontSize: 12)),
onTap: () {
launchUrlString('https://github.com/Solsynth/HyperNet.Surface');
},
)
],
),
),

View File

@ -191,7 +191,7 @@ packages:
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
dependency: "direct main"
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
@ -1083,7 +1083,7 @@ packages:
source: hosted
version: "0.2.1+2"
image_picker_platform_interface:
dependency: transitive
dependency: "direct main"
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"

View File

@ -121,6 +121,8 @@ dependencies:
tray_manager: ^0.3.2
hotkey_manager: ^0.2.3
image_picker_android: ^0.8.12+20
cached_network_image_platform_interface: ^4.1.1
image_picker_platform_interface: ^2.10.1
dev_dependencies:
flutter_test: