diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json
index a6f9463..c529402 100644
--- a/assets/translations/en-US.json
+++ b/assets/translations/en-US.json
@@ -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."
 }
diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json
index 74acb59..0f389f7 100644
--- a/assets/translations/zh-CN.json
+++ b/assets/translations/zh-CN.json
@@ -665,5 +665,8 @@
     "zero": "{} 次浏览",
     "one": "{} 次浏览",
     "other": "{} 次浏览"
-  }
+  },
+  "attachmentBillingUploaded": "已占用的字节数",
+  "attachmentBillingDiscount": "免费的字节数",
+  "attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。"
 }
diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json
index 660b0fd..88e49b1 100644
--- a/assets/translations/zh-HK.json
+++ b/assets/translations/zh-HK.json
@@ -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小時內上傳的文件大小超出免費空間才會適用扣費。"
 }
diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json
index 3a6a95a..ff9c846 100644
--- a/assets/translations/zh-TW.json
+++ b/assets/translations/zh-TW.json
@@ -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小時內上傳的文件大小超出免費空間才會適用扣費。"
 }
diff --git a/lib/screens/album.dart b/lib/screens/album.dart
index 95aaa10..e7bcd36 100644
--- a/lib/screens/album.dart
+++ b/lib/screens/album.dart
@@ -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,
diff --git a/lib/types/attachment.dart b/lib/types/attachment.dart
index c47da1f..0059337 100644
--- a/lib/types/attachment.dart
+++ b/lib/types/attachment.dart
@@ -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);
+}
diff --git a/lib/types/attachment.freezed.dart b/lib/types/attachment.freezed.dart
index 65db707..35f60d0 100644
--- a/lib/types/attachment.freezed.dart
+++ b/lib/types/attachment.freezed.dart
@@ -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;
+}
diff --git a/lib/types/attachment.g.dart b/lib/types/attachment.g.dart
index 1337a46..492aa7b 100644
--- a/lib/types/attachment.g.dart
+++ b/lib/types/attachment.g.dart
@@ -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,
+    };
diff --git a/pubspec.lock b/pubspec.lock
index 0f9aefd..b3ea847 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -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"
diff --git a/pubspec.yaml b/pubspec.yaml
index 301f7de..313aa3d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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: