✨ Sliding window pricing of attachment billing info displaying
This commit is contained in:
		| @@ -666,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." | ||||
| } | ||||
|   | ||||
| @@ -665,5 +665,8 @@ | ||||
|     "zero": "{} 次浏览", | ||||
|     "one": "{} 次浏览", | ||||
|     "other": "{} 次浏览" | ||||
|   } | ||||
|   }, | ||||
|   "attachmentBillingUploaded": "已占用的字节数", | ||||
|   "attachmentBillingDiscount": "免费的字节数", | ||||
|   "attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。" | ||||
| } | ||||
|   | ||||
| @@ -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小時內上傳的文件大小超出免費空間才會適用扣費。" | ||||
| } | ||||
|   | ||||
| @@ -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小時內上傳的文件大小超出免費空間才會適用扣費。" | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|     }; | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user