Compare commits
No commits in common. "main" and "v1.0.49" have entirely different histories.
|
|
@ -358,15 +358,6 @@ class _PendingAnalysisScreenState extends ConsumerState<PendingAnalysisScreen> {
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: appColors.divider,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Icon(LucideIcons.image, color: appColors.iconSubtle),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import '../providers/theme_provider.dart';
|
import '../providers/theme_provider.dart';
|
||||||
import '../theme/app_colors.dart';
|
|
||||||
import '../widgets/settings/display_settings_section.dart';
|
import '../widgets/settings/display_settings_section.dart';
|
||||||
import '../widgets/settings/other_settings_section.dart';
|
import '../widgets/settings/other_settings_section.dart';
|
||||||
import '../widgets/settings/backup_settings_section.dart';
|
import '../widgets/settings/backup_settings_section.dart';
|
||||||
|
|
@ -23,7 +22,7 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userProfile = ref.watch(userProfileProvider);
|
final userProfile = ref.watch(userProfileProvider);
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
|
@ -36,9 +35,9 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
|
||||||
// Business Config Section
|
// Business Config Section
|
||||||
_buildSectionHeader(context, '価格設定', LucideIcons.briefcase),
|
_buildSectionHeader(context, '価格設定', LucideIcons.briefcase),
|
||||||
Card(
|
Card(
|
||||||
color: appColors.surfaceElevated,
|
color: isDark ? const Color(0xFF1E1E1E) : null,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(LucideIcons.percent, color: appColors.iconAccent),
|
leading: Icon(LucideIcons.percent, color: isDark ? Colors.orange[300] : Theme.of(context).primaryColor),
|
||||||
title: const Text('基本掛率'),
|
title: const Text('基本掛率'),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -46,15 +45,15 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
|
||||||
Text('×', style: TextStyle(
|
Text('×', style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: appColors.textSecondary,
|
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||||
)),
|
)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: appColors.surfaceSubtle,
|
color: isDark ? Colors.grey[800] : Colors.grey[100],
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: appColors.divider),
|
border: Border.all(color: isDark ? Colors.grey[700]! : Colors.grey[300]!),
|
||||||
),
|
),
|
||||||
child: DropdownButton<double>(
|
child: DropdownButton<double>(
|
||||||
value: userProfile.defaultMarkup,
|
value: userProfile.defaultMarkup,
|
||||||
|
|
@ -97,18 +96,18 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionHeader(BuildContext context, String title, IconData icon) {
|
Widget _buildSectionHeader(BuildContext context, String title, IconData icon) {
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 20, color: appColors.iconAccent),
|
Icon(icon, size: 20, color: isDark ? Colors.orange[300] : Theme.of(context).primaryColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: appColors.textPrimary,
|
color: isDark ? Colors.grey[300] : Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,6 @@ import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import '../models/sake_item.dart';
|
import '../models/sake_item.dart';
|
||||||
|
|
||||||
/// 復元前の安全バックアップ作成に失敗したことを示す例外。
|
|
||||||
/// UI 側でユーザーに中断/続行を選ばせるために使用する。
|
|
||||||
class PreRestoreBackupException implements Exception {
|
|
||||||
const PreRestoreBackupException();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'PreRestoreBackupException: pre-restore safety backup failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Google Driveへのバックアップ・復元を管理するサービス
|
/// Google Driveへのバックアップ・復元を管理するサービス
|
||||||
///
|
///
|
||||||
/// 【主な機能】
|
/// 【主な機能】
|
||||||
|
|
@ -334,11 +325,8 @@ class BackupService {
|
||||||
|
|
||||||
final driveApi = drive.DriveApi(authClient);
|
final driveApi = drive.DriveApi(authClient);
|
||||||
|
|
||||||
// 3. 現在のデータを退避(失敗したら呼び出し元に通知して中断させる)
|
// 3. 現在のデータを退避
|
||||||
final preBackupOk = await _createPreRestoreBackup();
|
await _createPreRestoreBackup();
|
||||||
if (!preBackupOk) {
|
|
||||||
throw const PreRestoreBackupException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Google Driveからダウンロード
|
// 4. Google Driveからダウンロード
|
||||||
final zipFile = await _downloadFromDrive(driveApi);
|
final zipFile = await _downloadFromDrive(driveApi);
|
||||||
|
|
@ -354,55 +342,26 @@ class BackupService {
|
||||||
await zipFile.delete();
|
await zipFile.delete();
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
} on PreRestoreBackupException {
|
|
||||||
rethrow;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('[RESTORE] Restore error: $error');
|
debugPrint('[RESTORE] Restore error: $error');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 復元前に現在のデータを退避する。成功したら true、失敗したら false を返す。
|
/// 復元前に現在のデータを退避
|
||||||
Future<bool> _createPreRestoreBackup() async {
|
Future<void> _createPreRestoreBackup() async {
|
||||||
try {
|
try {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final backupPath = path.join(tempDir.path, 'pre_restore_backup.zip');
|
final backupPath = path.join(tempDir.path, 'pre_restore_backup.zip');
|
||||||
|
|
||||||
final zipFile = await _createBackupZip();
|
final zipFile = await _createBackupZip();
|
||||||
if (zipFile == null) {
|
if (zipFile != null) {
|
||||||
debugPrint('[RESTORE] Pre-restore backup: ZIP creation failed');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await zipFile.copy(backupPath);
|
await zipFile.copy(backupPath);
|
||||||
await zipFile.delete();
|
await zipFile.delete();
|
||||||
debugPrint('[RESTORE] Pre-restore backup saved: $backupPath');
|
debugPrint('[RESTORE] Pre-restore backup saved: $backupPath');
|
||||||
return true;
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('[RESTORE] Pre-restore backup error: $error');
|
debugPrint('[RESTORE] Pre-restore backup error: $error');
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 安全バックアップをスキップして復元を強行する(ユーザーが警告を承諾した場合)。
|
|
||||||
Future<bool> restoreBackupSkippingPreBackup() async {
|
|
||||||
try {
|
|
||||||
final account = _googleSignIn.currentUser;
|
|
||||||
if (account == null) return false;
|
|
||||||
|
|
||||||
final authClient = await _googleSignIn.authenticatedClient();
|
|
||||||
if (authClient == null) return false;
|
|
||||||
|
|
||||||
final driveApi = drive.DriveApi(authClient);
|
|
||||||
|
|
||||||
final zipFile = await _downloadFromDrive(driveApi);
|
|
||||||
if (zipFile == null) return false;
|
|
||||||
|
|
||||||
final success = await _restoreFromZip(zipFile);
|
|
||||||
await zipFile.delete();
|
|
||||||
return success;
|
|
||||||
} catch (error) {
|
|
||||||
debugPrint('[RESTORE] Force restore error: $error');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,14 +393,7 @@ class BackupService {
|
||||||
|
|
||||||
// 3. ストリームをファイルに書き込み
|
// 3. ストリームをファイルに書き込み
|
||||||
final sink = downloadFile.openWrite();
|
final sink = downloadFile.openWrite();
|
||||||
await media.stream.pipe(sink).timeout(
|
await media.stream.pipe(sink);
|
||||||
const Duration(minutes: 3),
|
|
||||||
onTimeout: () {
|
|
||||||
sink.close();
|
|
||||||
try { downloadFile.deleteSync(); } catch (_) {}
|
|
||||||
throw TimeoutException('Backup download timed out after 3 minutes');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
debugPrint('[RESTORE] Download complete: $downloadPath');
|
debugPrint('[RESTORE] Download complete: $downloadPath');
|
||||||
return downloadFile;
|
return downloadFile;
|
||||||
|
|
@ -532,7 +484,7 @@ class BackupService {
|
||||||
isUserEdited: data['userData']['isUserEdited'] as bool,
|
isUserEdited: data['userData']['isUserEdited'] as bool,
|
||||||
price: data['userData']['price'] as int?,
|
price: data['userData']['price'] as int?,
|
||||||
costPrice: data['userData']['costPrice'] as int?,
|
costPrice: data['userData']['costPrice'] as int?,
|
||||||
markup: (data['userData']['markup'] as num?)?.toDouble() ?? 3.0,
|
markup: (data['userData']['markup'] as num).toDouble(),
|
||||||
priceVariants: data['userData']['priceVariants'] != null
|
priceVariants: data['userData']['priceVariants'] != null
|
||||||
? Map<String, int>.from(data['userData']['priceVariants'] as Map)
|
? Map<String, int>.from(data['userData']['priceVariants'] as Map)
|
||||||
: null,
|
: null,
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,6 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
|
||||||
_isEditing,
|
_isEditing,
|
||||||
suffixIcon: LucideIcons.calendar,
|
suffixIcon: LucideIcons.calendar,
|
||||||
onSuffixTap: () => _showDatePicker(context),
|
onSuffixTap: () => _showDatePicker(context),
|
||||||
helperText: '例: 2023-10',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -304,11 +303,10 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
|
||||||
void _showDatePicker(BuildContext context) {
|
void _showDatePicker(BuildContext context) {
|
||||||
if (!_isEditing) return;
|
if (!_isEditing) return;
|
||||||
|
|
||||||
// Parse current value or use now (AI出力 "2023.10" とユーザー入力 "2023-10" の両形式に対応)
|
// Parse current value or use now
|
||||||
DateTime initialDate = DateTime.now();
|
DateTime initialDate = DateTime.now();
|
||||||
try {
|
try {
|
||||||
final normalized = _manufacturingController.text.replaceAll('.', '-');
|
final parts = _manufacturingController.text.split('-');
|
||||||
final parts = normalized.split('-');
|
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
final year = int.parse(parts[0]);
|
final year = int.parse(parts[0]);
|
||||||
final month = int.parse(parts[1]);
|
final month = int.parse(parts[1]);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import '../../providers/sake_list_provider.dart';
|
|
||||||
import '../../providers/theme_provider.dart';
|
|
||||||
import '../../services/backup_service.dart';
|
import '../../services/backup_service.dart';
|
||||||
import '../../theme/app_colors.dart';
|
import '../../theme/app_colors.dart';
|
||||||
|
|
||||||
class BackupSettingsSection extends ConsumerStatefulWidget {
|
class BackupSettingsSection extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
const BackupSettingsSection({
|
const BackupSettingsSection({
|
||||||
|
|
@ -15,12 +12,12 @@ class BackupSettingsSection extends ConsumerStatefulWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<BackupSettingsSection> createState() => _BackupSettingsSectionState();
|
State<BackupSettingsSection> createState() => _BackupSettingsSectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
|
enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
|
||||||
|
|
||||||
class _BackupSettingsSectionState extends ConsumerState<BackupSettingsSection> {
|
class _BackupSettingsSectionState extends State<BackupSettingsSection> {
|
||||||
final BackupService _backupService = BackupService();
|
final BackupService _backupService = BackupService();
|
||||||
_BackupState _state = _BackupState.idle;
|
_BackupState _state = _BackupState.idle;
|
||||||
|
|
||||||
|
|
@ -31,11 +28,7 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initBackupService() async {
|
Future<void> _initBackupService() async {
|
||||||
try {
|
|
||||||
await _backupService.init();
|
await _backupService.init();
|
||||||
} catch (e) {
|
|
||||||
debugPrint('[Backup] Init error (silent sign-in failed): $e');
|
|
||||||
}
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +132,7 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
|
||||||
Future<void> _restoreBackup() async {
|
Future<void> _restoreBackup() async {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||||
|
// Note: hasBackup check is async
|
||||||
final hasBackup = await _backupService.hasBackupOnDrive();
|
final hasBackup = await _backupService.hasBackupOnDrive();
|
||||||
if (!hasBackup) {
|
if (!hasBackup) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -180,29 +173,11 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed != true || !mounted) return;
|
if (confirmed == true && mounted) {
|
||||||
await _executeRestore(forceSkipPreBackup: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 実際の復元処理。PreRestoreBackupException を受けた場合はダイアログで続行確認する。
|
|
||||||
Future<void> _executeRestore({required bool forceSkipPreBackup}) async {
|
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
|
||||||
|
|
||||||
setState(() => _state = _BackupState.restoring);
|
setState(() => _state = _BackupState.restoring);
|
||||||
|
final success = await _backupService.restoreBackup();
|
||||||
try {
|
|
||||||
final success = forceSkipPreBackup
|
|
||||||
? await _backupService.restoreBackupSkippingPreBackup()
|
|
||||||
: await _backupService.restoreBackup();
|
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _state = _BackupState.idle);
|
setState(() => _state = _BackupState.idle);
|
||||||
if (success) {
|
|
||||||
ref.invalidate(rawSakeListItemsProvider);
|
|
||||||
ref.invalidate(sakeSortOrderProvider);
|
|
||||||
ref.invalidate(userProfileProvider);
|
|
||||||
}
|
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(success ? '復元が完了しました' : '復元に失敗しました'),
|
content: Text(success ? '復元が完了しました' : '復元に失敗しました'),
|
||||||
|
|
@ -210,47 +185,6 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} on PreRestoreBackupException {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() => _state = _BackupState.idle);
|
|
||||||
|
|
||||||
// 事前バックアップ失敗 → ユーザーに警告して続行するか確認
|
|
||||||
final continueAnyway = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Icon(LucideIcons.alertTriangle, color: appColors.error, size: 24),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text('安全バックアップに失敗'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: const Text(
|
|
||||||
'復元前の安全コピー作成に失敗しました。\n'
|
|
||||||
'このまま続行すると、現在のデータが失われた場合に\n'
|
|
||||||
'元に戻せない可能性があります。\n\n'
|
|
||||||
'続行しますか?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
child: const Text('中断'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: appColors.error,
|
|
||||||
foregroundColor: appColors.surfaceSubtle,
|
|
||||||
),
|
|
||||||
child: const Text('それでも続行'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (continueAnyway == true && mounted) {
|
|
||||||
await _executeRestore(forceSkipPreBackup: true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"version": "v1.0.49",
|
"version": "v1.0.47",
|
||||||
"name": "Ponshu Room 1.0.49 (2026-04-23)",
|
"name": "Ponshu Room 1.0.47 (2026-04-23)",
|
||||||
"date": "2026-04-23",
|
"date": "2026-04-23",
|
||||||
"apks": {
|
"apks": {
|
||||||
"maita": {
|
"maita": {
|
||||||
"lite": {
|
"lite": {
|
||||||
"filename": "ponshu_room_consumer_maita.apk",
|
"filename": "ponshu_room_consumer_maita.apk",
|
||||||
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.49/ponshu_room_consumer_maita.apk",
|
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.47/ponshu_room_consumer_maita.apk",
|
||||||
"size_mb": 91
|
"size_mb": 91
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eiji": {
|
"eiji": {
|
||||||
"lite": {
|
"lite": {
|
||||||
"filename": "ponshu_room_consumer_eiji.apk",
|
"filename": "ponshu_room_consumer_eiji.apk",
|
||||||
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.49/ponshu_room_consumer_eiji.apk",
|
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.47/ponshu_room_consumer_eiji.apk",
|
||||||
"size_mb": 91
|
"size_mb": 91
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue