You've already forked tcna-packages
mirror of
https://github.com/toyota-connected/tcna-packages.git
synced 2026-06-21 07:19:36 -07:00
Merge pull request #74 from AhmedAdelWafdy7/flatpak/responsive
flatpak: fix UI Responsiveness and handle storage
This commit is contained in:
@@ -378,26 +378,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -631,10 +631,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.6"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -655,10 +655,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -773,5 +773,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -47,7 +47,8 @@ class FlatpakApp extends StatelessWidget {
|
||||
),
|
||||
|
||||
BlocProvider<SystemInfoCubit>(
|
||||
create: (context) => sl<SystemInfoCubit>(),
|
||||
create: (context) => sl<SystemInfoCubit>()..loadSystemInfo(),
|
||||
lazy: false,
|
||||
),
|
||||
],
|
||||
child: PermissionHandlerWrapper(
|
||||
|
||||
@@ -72,13 +72,15 @@ class InstallationCubit extends Cubit<InstallationState> {
|
||||
result.fold(
|
||||
(failure) {
|
||||
debugPrint('[InstallationCubit] Failed to start install: ${failure.message}');
|
||||
_cleanup(shortId);
|
||||
appStatusCubit.updateAppStatus(shortId, AppStatus.notInstalled);
|
||||
emit(InstallationFailure(
|
||||
appId: shortId,
|
||||
error: failure.message,
|
||||
operation: 'install',
|
||||
));
|
||||
if (state is! InstallationInsufficientSpace) {
|
||||
_cleanup(shortId);
|
||||
appStatusCubit.updateAppStatus(shortId, AppStatus.notInstalled);
|
||||
emit(InstallationFailure(
|
||||
appId: shortId,
|
||||
error: failure.message,
|
||||
operation: 'install',
|
||||
));
|
||||
}
|
||||
},
|
||||
(success) {
|
||||
debugPrint('[InstallationCubit] Install initiated for $shortId');
|
||||
@@ -197,7 +199,8 @@ class InstallationCubit extends Cubit<InstallationState> {
|
||||
}
|
||||
|
||||
void _handleTransactionEvent(String appId, FlatpakEventModel event) {
|
||||
final tracker = _operationTrackers[appId];
|
||||
final appIdUtils = AppIdUtils.extractShortId(appId);
|
||||
final tracker = _operationTrackers[appIdUtils];
|
||||
if (tracker == null) return;
|
||||
|
||||
switch (event.type) {
|
||||
@@ -223,10 +226,10 @@ class InstallationCubit extends Cubit<InstallationState> {
|
||||
? AppStatus.updating
|
||||
: AppStatus.installing;
|
||||
|
||||
appStatusCubit.updateAppStatus(appId, appStatus, progress: progress);
|
||||
appStatusCubit.updateAppStatus(appIdUtils, appStatus, progress: progress);
|
||||
|
||||
emit(InstallationInProgress(
|
||||
appId: appId,
|
||||
appId: appIdUtils,
|
||||
status: InstallationStatus.downloading,
|
||||
progress: progress,
|
||||
message: event.message ?? 'Processing...',
|
||||
@@ -238,25 +241,35 @@ class InstallationCubit extends Cubit<InstallationState> {
|
||||
break;
|
||||
|
||||
case FlatpakEventType.installComplete:
|
||||
appStatusCubit.markInstalled(appId);
|
||||
_completeOperation(appId, 'install');
|
||||
appStatusCubit.markInstalled(appIdUtils);
|
||||
_completeOperation(appIdUtils, 'install');
|
||||
break;
|
||||
|
||||
case FlatpakEventType.uninstallComplete:
|
||||
appStatusCubit.markUninstalled(appId);
|
||||
_completeOperation(appId, 'uninstall');
|
||||
appStatusCubit.markUninstalled(appIdUtils);
|
||||
_completeOperation(appIdUtils, 'uninstall');
|
||||
break;
|
||||
|
||||
case FlatpakEventType.updateComplete:
|
||||
appStatusCubit.markUpdated(appId);
|
||||
_completeOperation(appId, 'update');
|
||||
appStatusCubit.markUpdated(appIdUtils);
|
||||
_completeOperation(appIdUtils, 'update');
|
||||
break;
|
||||
|
||||
case FlatpakEventType.installFailed:
|
||||
case FlatpakEventType.uninstallFailed:
|
||||
case FlatpakEventType.updateFailed:
|
||||
final error = event.error ?? event.message ?? 'Unknown error';
|
||||
_failOperation(appId, tracker.operationType, error);
|
||||
_failOperation(appIdUtils, tracker.operationType, error);
|
||||
break;
|
||||
|
||||
case FlatpakEventType.insufficientSpace:
|
||||
appStatusCubit.updateAppStatus(appIdUtils, AppStatus.notInstalled);
|
||||
emit(InstallationInsufficientSpace(
|
||||
appId: appIdUtils,
|
||||
requiredMb: event.requiredMb ?? 0,
|
||||
availableMb: event.availableMb ?? 0,
|
||||
));
|
||||
_cleanup(appIdUtils);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -49,3 +49,18 @@ class InstallationFailure extends InstallationState {
|
||||
@override
|
||||
List<Object?> get props => [appId, error, operation];
|
||||
}
|
||||
|
||||
class InstallationInsufficientSpace extends InstallationState {
|
||||
final String appId;
|
||||
final int requiredMb;
|
||||
final int availableMb;
|
||||
|
||||
InstallationInsufficientSpace({
|
||||
required this.appId,
|
||||
required this.requiredMb,
|
||||
required this.availableMb,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [appId, requiredMb, availableMb];
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class SystemInfoCubit extends Cubit<SystemInfoState> {
|
||||
flatpakRepository.getSupportedArches(),
|
||||
flatpakRepository.getSystemInstallations(),
|
||||
flatpakRepository.getUserInstallation(),
|
||||
flatpakRepository.getSystemStorage(),
|
||||
]);
|
||||
|
||||
final versionResult = results[0];
|
||||
@@ -27,12 +28,14 @@ class SystemInfoCubit extends Cubit<SystemInfoState> {
|
||||
final archesResult = results[2];
|
||||
final installationsResult = results[3];
|
||||
final userInstallationResult = results[4];
|
||||
final systemStorageResult = results[5];
|
||||
|
||||
String version = '';
|
||||
String defaultArch = '';
|
||||
List<String> supportedArches = [];
|
||||
List<Installation> systemInstallations = [];
|
||||
Installation? userInstallation;
|
||||
Map<String, dynamic>? systemStorage;
|
||||
|
||||
versionResult.fold(
|
||||
(failure) => debugPrint(
|
||||
@@ -69,6 +72,13 @@ class SystemInfoCubit extends Cubit<SystemInfoState> {
|
||||
(i) => userInstallation = i as Installation,
|
||||
);
|
||||
|
||||
systemStorageResult.fold(
|
||||
(failure) => debugPrint(
|
||||
'[SystemInfoCubit] Failed to get system storage: ${failure.message}',
|
||||
),
|
||||
(s) => systemStorage = s as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
emit(
|
||||
SystemInfoLoaded(
|
||||
version: version,
|
||||
@@ -76,6 +86,7 @@ class SystemInfoCubit extends Cubit<SystemInfoState> {
|
||||
supportedArches: supportedArches,
|
||||
systemInstallations: systemInstallations,
|
||||
userInstallation: userInstallation,
|
||||
systemStorage: systemStorage,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class SystemInfoLoaded extends SystemInfoState {
|
||||
final List<String> supportedArches;
|
||||
final List<Installation> systemInstallations;
|
||||
final Installation? userInstallation;
|
||||
final Map<String, dynamic>? systemStorage;
|
||||
|
||||
const SystemInfoLoaded({
|
||||
required this.version,
|
||||
@@ -29,6 +30,7 @@ class SystemInfoLoaded extends SystemInfoState {
|
||||
required this.supportedArches,
|
||||
required this.systemInstallations,
|
||||
this.userInstallation,
|
||||
this.systemStorage,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -38,6 +40,7 @@ class SystemInfoLoaded extends SystemInfoState {
|
||||
supportedArches,
|
||||
systemInstallations,
|
||||
userInstallation,
|
||||
systemStorage,
|
||||
];
|
||||
|
||||
SystemInfoLoaded copyWith({
|
||||
@@ -46,6 +49,7 @@ class SystemInfoLoaded extends SystemInfoState {
|
||||
List<String>? supportedArches,
|
||||
List<Installation>? systemInstallations,
|
||||
Installation? userInstallation,
|
||||
Map<String, dynamic>? systemStorage,
|
||||
}) {
|
||||
return SystemInfoLoaded(
|
||||
version: version ?? this.version,
|
||||
@@ -53,6 +57,7 @@ class SystemInfoLoaded extends SystemInfoState {
|
||||
supportedArches: supportedArches ?? this.supportedArches,
|
||||
systemInstallations: systemInstallations ?? this.systemInstallations,
|
||||
userInstallation: userInstallation ?? this.userInstallation,
|
||||
systemStorage: systemStorage ?? this.systemStorage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,19 @@ class FlatpakPermissionDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getSystemStorage() async {
|
||||
try {
|
||||
final result = await _methodChannel.invokeMethod('getSystemStorage');
|
||||
if (result != null && result is Map) {
|
||||
return Map<String, dynamic>.from(result);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('[FlatpakPermissionData] Error getting system storage: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> grantPermission({
|
||||
required String appId,
|
||||
required FlatpakPermission permission,
|
||||
|
||||
@@ -18,6 +18,7 @@ enum FlatpakEventType {
|
||||
uninstallationStarted,
|
||||
updateStarted,
|
||||
connectionEstablished,
|
||||
insufficientSpace,
|
||||
}
|
||||
|
||||
class FlatpakEventModel {
|
||||
@@ -38,6 +39,8 @@ class FlatpakEventModel {
|
||||
final bool? isMainApp;
|
||||
final bool? success;
|
||||
final List<OperationInfo>? operations;
|
||||
final int? availableMb;
|
||||
final int? requiredMb;
|
||||
|
||||
FlatpakEventModel({
|
||||
required this.type,
|
||||
@@ -57,6 +60,8 @@ class FlatpakEventModel {
|
||||
this.isMainApp,
|
||||
this.success,
|
||||
this.operations,
|
||||
this.availableMb,
|
||||
this.requiredMb,
|
||||
});
|
||||
|
||||
factory FlatpakEventModel.fromMap(Map<dynamic, dynamic> map) {
|
||||
@@ -70,7 +75,7 @@ class FlatpakEventModel {
|
||||
|
||||
final typeStr = map['type'] as String?;
|
||||
FlatpakEventType eventType = FlatpakEventType.unknown;
|
||||
|
||||
|
||||
// Parse event type
|
||||
if (typeStr != null) {
|
||||
switch (typeStr) {
|
||||
@@ -164,6 +169,10 @@ class FlatpakEventModel {
|
||||
eventType = FlatpakEventType.transactionReady;
|
||||
break;
|
||||
|
||||
case 'insufficient_space':
|
||||
eventType = FlatpakEventType.insufficientSpace;
|
||||
break;
|
||||
|
||||
default:
|
||||
eventType = FlatpakEventType.unknown;
|
||||
}
|
||||
@@ -202,6 +211,8 @@ class FlatpakEventModel {
|
||||
isMainApp: toBool(map['is_main_app']),
|
||||
success: toBool(map['success']),
|
||||
operations: ops,
|
||||
availableMb: map['available_mb'] as int?,
|
||||
requiredMb: map['required_mb'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ abstract class FlatpakRepository {
|
||||
Future<Either<Failure, List<String>>> getSupportedArches();
|
||||
Future<Either<Failure, List<Installation>>> getSystemInstallations();
|
||||
Future<Either<Failure, Installation>> getUserInstallation();
|
||||
Future<Either<Failure, Map<String, dynamic>>> getSystemStorage();
|
||||
|
||||
// Application Discovery
|
||||
Future<Either<Failure, List<Application>>> getApplicationsInstalled();
|
||||
|
||||
@@ -103,6 +103,16 @@ class FlatpakRepositoryImpl implements FlatpakRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> getSystemStorage() async {
|
||||
final result = await permissionDataSource.getSystemStorage();
|
||||
if (result != null) {
|
||||
return Right(result);
|
||||
} else {
|
||||
return const Left(PlatformFailure("Failed to get storage Info"));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Application>>> getApplicationsInstalled() async {
|
||||
try {
|
||||
|
||||
@@ -274,7 +274,7 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
height: Responsive.scaleWithConstraints(context, 200, minSize: 180, maxSize: 260),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
@@ -294,7 +294,7 @@ class _DiscoverScreenState extends State<DiscoverScreen> {
|
||||
|
||||
Widget _buildFeaturedCard(Application app) {
|
||||
return SizedBox(
|
||||
width: 300,
|
||||
width: Responsive.scaleWithConstraints(context, 300, minSize: 260, maxSize: 380),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: BackdropFilter(
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../widgets/category_section.dart';
|
||||
import '../widgets/hero_widget.dart';
|
||||
import '../widgets/top_bar.dart';
|
||||
import '../../app_router.dart';
|
||||
import '../../responsive.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -88,10 +89,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error, size: 64, color: Colors.red),
|
||||
const SizedBox(height: 16),
|
||||
Icon(Icons.error, size: Responsive.scale(context, 64), color: Colors.red),
|
||||
Responsive.vGap(context, 16),
|
||||
Text('Error: ${state.message}'),
|
||||
const SizedBox(height: 16),
|
||||
Responsive.vGap(context, 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadData,
|
||||
child: const Text('Retry'),
|
||||
@@ -138,7 +139,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onTap: (app) => _navigateToApp(app, context),
|
||||
onInstall: (app) => _handleInstall(app, context),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Responsive.vGap(context, 32),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../responsive.dart';
|
||||
import '../../business_logic/app_launch/app_launch_cubit.dart';
|
||||
import '../../business_logic/app_launch/app_launch_state.dart';
|
||||
import '../../business_logic/app_status/app_status_cubit.dart';
|
||||
@@ -12,6 +13,10 @@ import '../../business_logic/discovery/discovery_cubit.dart';
|
||||
import '../../business_logic/installation/installation_cubit.dart';
|
||||
import '../../business_logic/installation/installation_state.dart';
|
||||
import '../../business_logic/discovery/discovery_state.dart';
|
||||
import '../../business_logic/system_info/system_info_cubit.dart';
|
||||
import '../../business_logic/system_info/system_info_state.dart';
|
||||
import '../../business_logic/system_info/system_info_cubit.dart';
|
||||
import '../../business_logic/system_info/system_info_state.dart';
|
||||
import '../../data/models/application_model.dart';
|
||||
import '../../helpers/id_utils.dart';
|
||||
import '../../app_router.dart';
|
||||
@@ -72,11 +77,13 @@ class _InstalledScreenState extends State<InstalledScreen>
|
||||
void _loadData() {
|
||||
widget.appStatusCubit.loadAppStatus();
|
||||
widget.discoveryCubit.loadAllApps();
|
||||
context.read<SystemInfoCubit>().loadSystemInfo();
|
||||
}
|
||||
|
||||
void _refreshData() {
|
||||
widget.appStatusCubit.refresh();
|
||||
widget.discoveryCubit.loadAllApps();
|
||||
context.read<SystemInfoCubit>().refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -186,7 +193,12 @@ class _InstalledScreenState extends State<InstalledScreen>
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 20),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
Responsive.scaleWithConstraints(context, 24, minSize: 16, maxSize: 40),
|
||||
Responsive.scaleWithConstraints(context, 16, minSize: 12, maxSize: 24),
|
||||
Responsive.scaleWithConstraints(context, 24, minSize: 16, maxSize: 40),
|
||||
Responsive.scaleWithConstraints(context, 20, minSize: 12, maxSize: 28),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
@@ -378,127 +390,148 @@ class _InstalledScreenState extends State<InstalledScreen>
|
||||
}
|
||||
|
||||
Widget _buildStorageSection(BuildContext context, List<Application> apps) {
|
||||
final totalBytes = apps.fold<int>(0, (sum, app) => sum + app.installedSize);
|
||||
final usedGB = (totalBytes / (1024 * 1024 * 1024)).toStringAsFixed(1);
|
||||
const totalSpace = 200.0;
|
||||
final used = double.parse(usedGB);
|
||||
final available = totalSpace - used;
|
||||
final percentage = ((used / totalSpace) * 100).toInt();
|
||||
return BlocBuilder<SystemInfoCubit, SystemInfoState>(
|
||||
builder: (context, systemInfoState) {
|
||||
double totalSpace = 200.0;
|
||||
double available = 0.0;
|
||||
double used = 0.0;
|
||||
|
||||
final appsTotalBytes = apps.fold<int>(0, (sum, app) => sum + app.installedSize);
|
||||
final appsUsedGB = appsTotalBytes / (1024 * 1024 * 1024);
|
||||
|
||||
return GlassContainer(
|
||||
borderRadius: 24,
|
||||
blur: 20,
|
||||
color: Colors.white.withValues(alpha: 0.4),
|
||||
borderOpacity: 0.4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
if (systemInfoState is SystemInfoLoaded && systemInfoState.systemStorage != null) {
|
||||
final storage = systemInfoState.systemStorage!;
|
||||
final totalBytesSys = storage['total'] as int? ?? 1;
|
||||
final availableBytesSys = storage['available'] as int? ?? 0;
|
||||
|
||||
totalSpace = totalBytesSys / (1024.0 * 1024.0 * 1024.0);
|
||||
available = availableBytesSys / (1024.0 * 1024.0 * 1024.0);
|
||||
used = totalSpace - available;
|
||||
} else {
|
||||
// Fallback if native system info isn't available
|
||||
used = appsUsedGB;
|
||||
available = totalSpace - used;
|
||||
}
|
||||
|
||||
final percentage = totalSpace > 0 ? ((used / totalSpace) * 100).toInt().clamp(0, 100) : 0;
|
||||
final usedGBStr = used.toStringAsFixed(1);
|
||||
final availableGBStr = available.toStringAsFixed(1);
|
||||
|
||||
return GlassContainer(
|
||||
borderRadius: 24,
|
||||
blur: 20,
|
||||
color: Colors.white.withValues(alpha: 0.4),
|
||||
borderOpacity: 0.4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.storage_rounded, size: 20, color: Colors.grey[700]),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Storage Usage',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
offset: const Offset(0, 1),
|
||||
blurRadius: 0,
|
||||
spreadRadius: 0,
|
||||
)
|
||||
]
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: (used / totalSpace).clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF60A5FA), Color(0xFF2563EB)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withValues(alpha: 0.4),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.storage_rounded, size: 20, color: Colors.grey[700]),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'System Storage',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Used: $usedGB GB',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w500,
|
||||
Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
]
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: totalSpace > 0 ? (used / totalSpace).clamp(0.0, 1.0) : 0.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF60A5FA), Color(0xFF2563EB)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withValues(alpha: 0.3),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Free: ${available.toStringAsFixed(1)} GB',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Used: $usedGBStr GB',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[800],
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Free: $availableGBStr GB',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF2563EB),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
'$percentage%',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2563EB),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
'${percentage}%',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2563EB),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppsList(BuildContext context, List<Application> apps, {required bool showUpdateButton}) {
|
||||
return Column(
|
||||
children: apps.map((app) => _buildAppItem(context, app, showUpdateButton)).toList(),
|
||||
|
||||
@@ -86,19 +86,8 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cardWidth = Responsive.scaleWithConstraints(
|
||||
context,
|
||||
220,
|
||||
minSize: 200,
|
||||
maxSize: 250,
|
||||
);
|
||||
|
||||
final cardHeight = Responsive.scaleWithConstraints(
|
||||
context,
|
||||
220,
|
||||
minSize: 200,
|
||||
maxSize: 250,
|
||||
);
|
||||
final cardWidth = Responsive.scale(context, 220);
|
||||
final cardHeight = Responsive.scale(context, 220);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _hoverController,
|
||||
@@ -113,11 +102,9 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
child: Container(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
margin: EdgeInsets.all(
|
||||
Responsive.scale(context, 12.0).clamp(10.0, 16.0),
|
||||
),
|
||||
margin: Responsive.paddingAll(context, 12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 16)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
@@ -127,7 +114,7 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 16)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
@@ -135,7 +122,7 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
color: _isHovered
|
||||
? Colors.white.withValues(alpha: 0.35)
|
||||
: Colors.white.withValues(alpha: 0.25),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 16)),
|
||||
border: Border.all(
|
||||
color: _isHovered
|
||||
? Colors.white.withValues(alpha: 0.4)
|
||||
@@ -144,9 +131,7 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
Responsive.scale(context, 12.0).clamp(10.0, 16.0),
|
||||
),
|
||||
padding: Responsive.paddingAll(context, 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -168,12 +153,12 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
Widget _buildAppIcon(BuildContext context, Application app) {
|
||||
final iconSize = Responsive.scale(context, 92).clamp(64.0, 128.0);
|
||||
final iconSize = Responsive.scale(context, 92);
|
||||
return Container(
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 12)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
@@ -183,7 +168,7 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 12)),
|
||||
child: _buildIcon(iconSize),
|
||||
),
|
||||
);
|
||||
@@ -238,19 +223,19 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
_getAppName(app),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: Responsive.scale(context, 20.0).clamp(16.0, 24.0),
|
||||
fontSize: Responsive.fontSize(context, 20.0),
|
||||
fontFamily: 'khand',
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Responsive.vGap(context, 4),
|
||||
Text(
|
||||
_getAppSummary(app),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color
|
||||
?.withValues(alpha: 0.7),
|
||||
fontSize: Responsive.scale(context, 12.0).clamp(10.0, 14.0),
|
||||
fontSize: Responsive.fontSize(context, 12.0),
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
maxLines: 1,
|
||||
@@ -259,10 +244,10 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Responsive.hGap(context, 8),
|
||||
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 130),
|
||||
constraints: BoxConstraints(maxWidth: Responsive.scale(context, 130)),
|
||||
child: _buildActionButton(context),
|
||||
),
|
||||
],
|
||||
@@ -301,18 +286,19 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
? () => _handleButtonTap(context, appStatus)
|
||||
: null,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 20)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
padding: Responsive.paddingSymmetric(
|
||||
context,
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: config.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 20)),
|
||||
border: Border.all(
|
||||
width: 1.5,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
@@ -329,8 +315,8 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
return Transform.rotate(
|
||||
angle: _rotationAnimation.value * 2.0 * 3.141592653589793,
|
||||
child: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
width: Responsive.scale(context, 14),
|
||||
height: Responsive.scale(context, 14),
|
||||
child: CircularProgressIndicator(
|
||||
value: progress != null && progress > 0 ? progress : null,
|
||||
strokeWidth: 2,
|
||||
@@ -342,19 +328,21 @@ class _AppCardState extends State<AppCard> with TickerProviderStateMixin {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Responsive.hGap(context, 8),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
config.text,
|
||||
style: TextStyle(
|
||||
color: config.textColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
fontSize: 14,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
config.text,
|
||||
style: TextStyle(
|
||||
color: config.textColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
fontSize: Responsive.fontSize(context, 14),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../responsive.dart';
|
||||
|
||||
class AppInfo extends StatelessWidget {
|
||||
const AppInfo({
|
||||
@@ -27,7 +28,7 @@ class AppInfo extends StatelessWidget {
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: Responsive.paddingAll(context, 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -39,14 +40,16 @@ class AppInfo extends StatelessWidget {
|
||||
"Version",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontSize: Responsive.fontSize(context, 12),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Responsive.vGap(context, 4),
|
||||
Text(
|
||||
version,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: Responsive.fontSize(context, 14),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
@@ -54,7 +57,7 @@ class AppInfo extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Responsive.vGap(context, 16),
|
||||
|
||||
/// Size
|
||||
Column(
|
||||
@@ -64,14 +67,16 @@ class AppInfo extends StatelessWidget {
|
||||
"Installed Size",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontSize: Responsive.fontSize(context, 12),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Responsive.vGap(context, 4),
|
||||
Text(
|
||||
size,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: Responsive.fontSize(context, 14),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
@@ -79,7 +84,7 @@ class AppInfo extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Responsive.vGap(context, 16),
|
||||
|
||||
/// Last Update
|
||||
Column(
|
||||
@@ -89,14 +94,16 @@ class AppInfo extends StatelessWidget {
|
||||
"Last Update",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontSize: Responsive.fontSize(context, 12),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Responsive.vGap(context, 4),
|
||||
Text(
|
||||
last_upadate,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: Responsive.fontSize(context, 14),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
@@ -115,7 +122,7 @@ class AppInfo extends StatelessWidget {
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: Responsive.paddingAll(context, 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -127,14 +134,16 @@ class AppInfo extends StatelessWidget {
|
||||
"License",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontSize: Responsive.fontSize(context, 12),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Responsive.vGap(context, 4),
|
||||
Text(
|
||||
License,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: Responsive.fontSize(context, 14),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
@@ -143,7 +152,7 @@ class AppInfo extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Responsive.vGap(context, 16),
|
||||
|
||||
/// Developer
|
||||
Column(
|
||||
@@ -153,17 +162,19 @@ class AppInfo extends StatelessWidget {
|
||||
"Developer",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontSize: Responsive.fontSize(context, 12),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Responsive.vGap(context, 4),
|
||||
GestureDetector(
|
||||
/// onTap: onTap, TODO: Add UrlLauncher
|
||||
child: Text(
|
||||
url,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.blue,
|
||||
fontSize: Responsive.fontSize(context, 14),
|
||||
decoration: TextDecoration.underline,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
@@ -173,7 +184,7 @@ class AppInfo extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Responsive.vGap(context, 16),
|
||||
|
||||
/// Content Rating
|
||||
Column(
|
||||
@@ -183,14 +194,16 @@ class AppInfo extends StatelessWidget {
|
||||
"Content Rating",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontSize: Responsive.fontSize(context, 12),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Responsive.vGap(context, 4),
|
||||
Text(
|
||||
content_rating,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: Responsive.fontSize(context, 14),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -11,7 +11,6 @@ import '../../business_logic/app_status/app_status_state.dart';
|
||||
import '../../business_logic/installation/installation_cubit.dart';
|
||||
import '../../business_logic/installation/installation_state.dart';
|
||||
import '../../data/models/application_model.dart';
|
||||
import '../../helpers/id_utils.dart';
|
||||
import 'package:flatpak_flutter_example/responsive.dart';
|
||||
import 'package:flatpak_flutter_example/presentation/widgets/screenshot_widget.dart';
|
||||
import 'app_info.dart';
|
||||
@@ -59,7 +58,8 @@ class AppscreenContent extends StatelessWidget {
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
padding: Responsive.paddingSymmetric(
|
||||
context,
|
||||
horizontal: 54,
|
||||
vertical: 15,
|
||||
),
|
||||
@@ -69,11 +69,11 @@ class AppscreenContent extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
Responsive.vGap(context, 18),
|
||||
_buildScreenshot(context),
|
||||
const SizedBox(height: 18),
|
||||
Responsive.vGap(context, 18),
|
||||
_buildInfo(context),
|
||||
const SizedBox(height: 40),
|
||||
Responsive.vGap(context, 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -95,12 +95,12 @@ class AppscreenContent extends StatelessWidget {
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 6.0),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 20.0, vertical: 6.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildAppIcon(context, app),
|
||||
const SizedBox(width: 14),
|
||||
Responsive.hGap(context, 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -110,7 +110,7 @@ class AppscreenContent extends StatelessWidget {
|
||||
app.name,
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: Responsive.scale(context, 18.0).clamp(15.0, 20.0),
|
||||
fontSize: Responsive.fontSize(context, 18.0),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'khand',
|
||||
height: 1.2,
|
||||
@@ -118,28 +118,27 @@ class AppscreenContent extends StatelessWidget {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Responsive.vGap(context, 5),
|
||||
if (developerName.isNotEmpty)
|
||||
Text(
|
||||
developerName,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF8B8B8B),
|
||||
fontSize:
|
||||
Responsive.scale(context, 13.0).clamp(11.0, 15.0),
|
||||
fontSize: Responsive.fontSize(context, 13.0),
|
||||
height: 1.1,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (categories.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Responsive.vGap(context, 8),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width - 200,
|
||||
maxWidth: Responsive.width(context) - Responsive.scale(context, 200),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
spacing: Responsive.scale(context, 6),
|
||||
runSpacing: Responsive.scale(context, 4),
|
||||
children: categories,
|
||||
),
|
||||
),
|
||||
@@ -147,7 +146,7 @@ class AppscreenContent extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Responsive.hGap(context, 16),
|
||||
_buildActionButtons(context),
|
||||
],
|
||||
),
|
||||
@@ -171,7 +170,7 @@ class AppscreenContent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildOpenButton(context),
|
||||
const SizedBox(width: 12),
|
||||
Responsive.hGap(context, 12),
|
||||
if (needsUpdate || status == AppStatus.updating)
|
||||
_buildUpdateButton(context)
|
||||
else
|
||||
@@ -289,14 +288,14 @@ class AppscreenContent extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildAppIcon(BuildContext context, Application app) {
|
||||
final cardSize = Responsive.scale(context, 80).clamp(68.0, 92.0);
|
||||
final cardSize = Responsive.scale(context, 80);
|
||||
final iconSize = cardSize * 0.7;
|
||||
|
||||
return Container(
|
||||
width: cardSize,
|
||||
height: cardSize,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 16)),
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -308,7 +307,7 @@ class AppscreenContent extends StatelessWidget {
|
||||
),
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 12)),
|
||||
child: _buildIcon(iconSize),
|
||||
),
|
||||
),
|
||||
@@ -365,13 +364,13 @@ class AppscreenContent extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
height: Responsive.responsiveValue(
|
||||
context,
|
||||
mobile: Responsive.height(context) * 0.3,
|
||||
tablet: Responsive.height(context) * 0.4,
|
||||
desktop: Responsive.height(context) * 0.5,
|
||||
).clamp(300.0, 600.0),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: Responsive.responsiveValue(context,
|
||||
mobile: 16.0, tablet: 24.0, desktop: 32.0),
|
||||
small: Responsive.height(context) * 0.4,
|
||||
medium: Responsive.height(context) * 0.5,
|
||||
large: Responsive.height(context) * 0.7,
|
||||
).clamp(300.0, 900.0),
|
||||
padding: Responsive.paddingSymmetric(
|
||||
context,
|
||||
horizontal: 32.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: Screenshot(images: images, captions: captions),
|
||||
@@ -388,7 +387,7 @@ class AppscreenContent extends StatelessWidget {
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 10.0),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 40.0, vertical: 10.0),
|
||||
alignment: Alignment.topLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.25),
|
||||
@@ -410,40 +409,40 @@ class AppscreenContent extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
"About this app",
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 18,
|
||||
fontSize: Responsive.fontSize(context, 18),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Responsive.vGap(context, 30),
|
||||
Text(
|
||||
description.isNotEmpty
|
||||
? description
|
||||
: "No description available.",
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4B5563),
|
||||
fontSize: 16,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF4B5563),
|
||||
fontSize: Responsive.fontSize(context, 16),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Responsive.vGap(context, 30),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
height: Responsive.scale(context, 1),
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
Padding(
|
||||
padding: Responsive.paddingAll(context, 12.0),
|
||||
child: Text(
|
||||
"App Info",
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 18,
|
||||
fontSize: Responsive.fontSize(context, 18),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
@@ -579,17 +578,17 @@ class AppscreenContent extends StatelessWidget {
|
||||
|
||||
return categories.take(3).where((c) => c != null).map((category) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 4)),
|
||||
border: Border.all(color: Colors.grey.shade300, width: 0.5),
|
||||
),
|
||||
child: Text(
|
||||
category.toString().trim(),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF374151),
|
||||
style: TextStyle(
|
||||
fontSize: Responsive.fontSize(context, 11),
|
||||
color: const Color(0xFF374151),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
@@ -715,7 +714,7 @@ class _GlassActionButton extends StatelessWidget {
|
||||
effectiveColor.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 12)),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
width: 1.0,
|
||||
@@ -723,14 +722,14 @@ class _GlassActionButton extends StatelessWidget {
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: effectiveColor.withValues(alpha: 0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
blurRadius: Responsive.scale(context, 12),
|
||||
offset: Offset(0, Responsive.scale(context, 4)),
|
||||
),
|
||||
],
|
||||
)
|
||||
: BoxDecoration(
|
||||
color: effectiveColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 12)),
|
||||
border: Border.all(
|
||||
color: effectiveColor.withValues(alpha: 0.4),
|
||||
width: 1.5,
|
||||
@@ -740,30 +739,29 @@ class _GlassActionButton extends StatelessWidget {
|
||||
final textColor = isFilled ? Colors.white : effectiveColor;
|
||||
|
||||
final showProgress = isLoading && progress != null && progress! > 0;
|
||||
final showSpinner = isLoading && (progress == null || progress! == 0);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 12)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 24, vertical: 12),
|
||||
decoration: decoration,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isLoading) ...[
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
width: Responsive.scale(context, 16),
|
||||
height: Responsive.scale(context, 16),
|
||||
child: CircularProgressIndicator(
|
||||
value: showProgress ? progress : null,
|
||||
strokeWidth: 2,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Responsive.hGap(context, 8),
|
||||
],
|
||||
|
||||
if (showProgress)
|
||||
@@ -771,19 +769,24 @@ class _GlassActionButton extends StatelessWidget {
|
||||
'${(progress! * 100).toInt()}%',
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 15,
|
||||
fontSize: Responsive.fontSize(context, 15),
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.3,
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: Responsive.fontSize(context, 15),
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -10,8 +10,10 @@ class AppscreenHead extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final barHeight = Responsive.scale(context, 100);
|
||||
|
||||
return Container(
|
||||
height: preferredSize.height,
|
||||
height: barHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.25),
|
||||
border: Border(
|
||||
@@ -32,7 +34,7 @@ class AppscreenHead extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 25),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 40, vertical: 25),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildBackWidget(context),
|
||||
@@ -47,22 +49,35 @@ class AppscreenHead extends StatelessWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(100);
|
||||
Size get preferredSize {
|
||||
try {
|
||||
final view = PlatformDispatcher.instance.implicitView ?? PlatformDispatcher.instance.views.first;
|
||||
final w = view.physicalSize.width / view.devicePixelRatio;
|
||||
final h = view.physicalSize.height / view.devicePixelRatio;
|
||||
|
||||
final wRatio = w / 1024.0;
|
||||
final hRatio = h / 768.0;
|
||||
final val = wRatio * hRatio;
|
||||
double scale = val <= 0 ? 1.0 : (val < 1 ? val + (1 - val) * 0.5 : val * 0.5 + 0.5);
|
||||
|
||||
if (w < 600) {
|
||||
scale = scale.clamp(0.45, 0.85);
|
||||
} else if (w < 1024) {
|
||||
scale = scale.clamp(0.70, 1.10);
|
||||
} else if (w < 1440) {
|
||||
scale = scale.clamp(0.90, 1.40);
|
||||
} else {
|
||||
scale = scale.clamp(1.10, 2.00);
|
||||
}
|
||||
return Size.fromHeight(100.0 * scale);
|
||||
} catch (_) {
|
||||
return const Size.fromHeight(100.0);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBackWidget(BuildContext context) {
|
||||
final backW = Responsive.scaleWithConstraints(
|
||||
context,
|
||||
26,
|
||||
minSize: 22,
|
||||
maxSize: 28,
|
||||
);
|
||||
|
||||
final backH = Responsive.scaleWithConstraints(
|
||||
context,
|
||||
26,
|
||||
minSize: 22,
|
||||
maxSize: 28,
|
||||
);
|
||||
final backW = Responsive.scale(context, 26);
|
||||
final backH = Responsive.scale(context, 26);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
@@ -71,18 +86,16 @@ class AppscreenHead extends StatelessWidget implements PreferredSizeWidget {
|
||||
SizedBox(
|
||||
height: backH,
|
||||
width: backW,
|
||||
child: Icon(CupertinoIcons.back, color: Colors.black),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Icon(CupertinoIcons.back, color: Colors.black, size: Responsive.scale(context, 24)),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Responsive.hGap(context, 10),
|
||||
Text(
|
||||
appname,
|
||||
style: TextStyle(
|
||||
fontSize: Responsive.scaleWithConstraints(
|
||||
context,
|
||||
20,
|
||||
minSize: 16,
|
||||
maxSize: 24,
|
||||
),
|
||||
fontSize: Responsive.fontSize(context, 20),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'khand',
|
||||
color: Colors.black,
|
||||
@@ -94,19 +107,8 @@ class AppscreenHead extends StatelessWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
Widget _buildEngageWidget(BuildContext context) {
|
||||
final engageW = Responsive.scaleWithConstraints(
|
||||
context,
|
||||
56,
|
||||
minSize: 48,
|
||||
maxSize: 64,
|
||||
);
|
||||
|
||||
final engageH = Responsive.scaleWithConstraints(
|
||||
context,
|
||||
20,
|
||||
minSize: 16,
|
||||
maxSize: 24,
|
||||
);
|
||||
final engageW = Responsive.scale(context, 56);
|
||||
final engageH = Responsive.scale(context, 20);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
@@ -117,34 +119,14 @@ class AppscreenHead extends StatelessWidget implements PreferredSizeWidget {
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/like.svg',
|
||||
width: Responsive.scaleWithConstraints(
|
||||
context,
|
||||
20,
|
||||
minSize: 16,
|
||||
maxSize: 24,
|
||||
),
|
||||
height: Responsive.scaleWithConstraints(
|
||||
context,
|
||||
20,
|
||||
minSize: 16,
|
||||
maxSize: 24,
|
||||
),
|
||||
width: Responsive.scale(context, 20),
|
||||
height: Responsive.scale(context, 20),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Responsive.hGap(context, 12),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/share.svg',
|
||||
width: Responsive.scaleWithConstraints(
|
||||
context,
|
||||
20,
|
||||
minSize: 16,
|
||||
maxSize: 24,
|
||||
),
|
||||
height: Responsive.scaleWithConstraints(
|
||||
context,
|
||||
20,
|
||||
minSize: 16,
|
||||
maxSize: 24,
|
||||
),
|
||||
width: Responsive.scale(context, 20),
|
||||
height: Responsive.scale(context, 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -26,17 +26,23 @@ class CategorySection extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 32),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
category_heading,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF111827),
|
||||
fontSize: Responsive.scale(context, 24).clamp(16, 28),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'khand',
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
category_heading,
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF111827),
|
||||
fontSize: Responsive.fontSize(context, 24),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'khand',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
@@ -45,7 +51,7 @@ class CategorySection extends StatelessWidget {
|
||||
"See All",
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF2563EB),
|
||||
fontSize: Responsive.scale(context, 20).clamp(14, 24),
|
||||
fontSize: Responsive.fontSize(context, 20),
|
||||
fontWeight: FontWeight.normal,
|
||||
fontFamily: 'general-sans',
|
||||
),
|
||||
@@ -54,35 +60,38 @@ class CategorySection extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(height: 234, child: _buildContent(context)),
|
||||
Responsive.vGap(context, 20),
|
||||
SizedBox(
|
||||
height: Responsive.scale(context, 234),
|
||||
child: _buildContent(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
if (isLoading && apps.isEmpty) {
|
||||
return _buildLoadingState();
|
||||
return _buildLoadingState(context);
|
||||
}
|
||||
|
||||
if (apps.isEmpty && !isLoading) {
|
||||
return _buildEmptyState();
|
||||
return _buildEmptyState(context);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 16),
|
||||
itemCount: apps.length + (isLoading ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= apps.length) {
|
||||
return _buildLoadingCard();
|
||||
return _buildLoadingCard(context);
|
||||
}
|
||||
|
||||
final app = apps[index];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 8.0),
|
||||
child: AppCard(
|
||||
application: app,
|
||||
onTap: () => onTap(app),
|
||||
@@ -92,64 +101,64 @@ class CategorySection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState() {
|
||||
Widget _buildLoadingState(BuildContext context) {
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 16),
|
||||
itemCount: 5,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _buildLoadingCard(),
|
||||
padding: Responsive.paddingSymmetric(context, horizontal: 8.0),
|
||||
child: _buildLoadingCard(context),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard() {
|
||||
Widget _buildLoadingCard(BuildContext context) {
|
||||
return Container(
|
||||
width: 160,
|
||||
width: Responsive.scale(context, 160),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 12)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
width: Responsive.scale(context, 64),
|
||||
height: Responsive.scale(context, 64),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Responsive.vGap(context, 12),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 16,
|
||||
width: Responsive.scale(context, 100),
|
||||
height: Responsive.scale(context, 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 4)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Responsive.vGap(context, 8),
|
||||
Container(
|
||||
width: 80,
|
||||
height: 14,
|
||||
width: Responsive.scale(context, 80),
|
||||
height: Responsive.scale(context, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 4)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Responsive.vGap(context, 12),
|
||||
Container(
|
||||
width: 120,
|
||||
height: 36,
|
||||
width: Responsive.scale(context, 120),
|
||||
height: Responsive.scale(context, 36),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 18)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -157,21 +166,21 @@ class CategorySection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.apps, size: 48, color: Colors.grey[400]),
|
||||
const SizedBox(height: 8),
|
||||
Icon(Icons.apps, size: Responsive.scale(context, 48), color: Colors.grey[400]),
|
||||
Responsive.vGap(context, 8),
|
||||
Text(
|
||||
'No apps available in this category',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: Responsive.fontSize(context, 16)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Responsive.vGap(context, 8),
|
||||
Text(
|
||||
'Pull to refresh or check your connection',
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 12),
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: Responsive.fontSize(context, 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/permissions/permission_types.dart';
|
||||
import '../../data/models/flatpak_permission_model.dart';
|
||||
import '../../responsive.dart';
|
||||
import 'glassmorphism/glass_button.dart';
|
||||
import 'glassmorphism/glass_container.dart';
|
||||
|
||||
@@ -49,10 +50,7 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
curve: Curves.easeOutBack,
|
||||
);
|
||||
|
||||
_fadeAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
_fadeAnimation = CurvedAnimation(parent: _controller, curve: Curves.easeIn);
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
@@ -74,81 +72,91 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
scale: _scaleAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
insetPadding: const EdgeInsets.all(20),
|
||||
child: GlassContainer(
|
||||
borderRadius: 32,
|
||||
blur: 20,
|
||||
borderOpacity: 0.2,
|
||||
color: isDark
|
||||
? Colors.black.withValues(alpha: 0.6)
|
||||
: Colors.white.withValues(alpha: 0.7),
|
||||
child: Center(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildIconHeader(colorScheme, permission),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
Text(
|
||||
'Permission Required',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
// Ensure text is readable on glass
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
height: 1.5,
|
||||
),
|
||||
margin: Responsive.paddingAll(context, 20),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: Responsive.dialogMaxWidth(context),
|
||||
maxHeight: Responsive.dialogMaxHeight(context),
|
||||
),
|
||||
child: GlassContainer(
|
||||
borderRadius: Responsive.scale(context, 32),
|
||||
blur: 20,
|
||||
borderOpacity: 0.2,
|
||||
color: isDark
|
||||
? Colors.black.withValues(alpha: 0.6)
|
||||
: Colors.white.withValues(alpha: 0.7),
|
||||
child: Padding(
|
||||
padding: Responsive.paddingAll(context, 32),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: _getAppDisplayName(widget.event.appId),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
_buildIconHeader(context, colorScheme, permission),
|
||||
|
||||
Responsive.vGap(context, 28),
|
||||
|
||||
Text(
|
||||
'Permission Required',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: Responsive.fontSize(context, 24),
|
||||
// Ensure text is readable on glass
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const TextSpan(text: ' wants to access your '),
|
||||
TextSpan(
|
||||
text: permission.displayName.toLowerCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
Responsive.vGap(context, 12),
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
fontSize: Responsive.fontSize(context, 16),
|
||||
height: 1.5,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: _getAppDisplayName(widget.event.appId),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: ' wants to access your '),
|
||||
TextSpan(
|
||||
text: permission.displayName.toLowerCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Responsive.vGap(context, 24),
|
||||
|
||||
_buildDescriptionCard(context, permission),
|
||||
|
||||
if (widget.event.total != null &&
|
||||
widget.event.total! > 1) ...[
|
||||
Responsive.vGap(context, 24),
|
||||
_buildProgressIndicator(context, colorScheme),
|
||||
],
|
||||
|
||||
Responsive.vGap(context, 32),
|
||||
|
||||
// Action buttons
|
||||
_buildActionButtons(context, colorScheme, isDark),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildDescriptionCard(context, permission),
|
||||
|
||||
if (widget.event.total != null && widget.event.total! > 1) ...[
|
||||
const SizedBox(height: 24),
|
||||
_buildProgressIndicator(colorScheme),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Action buttons
|
||||
_buildActionButtons(colorScheme, isDark),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -158,33 +166,36 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
}
|
||||
|
||||
Widget _buildIconHeader(
|
||||
ColorScheme colorScheme, FlatpakPermission permission) {
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
FlatpakPermission permission,
|
||||
) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: Responsive.scale(context, 80),
|
||||
height: Responsive.scale(context, 80),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.primary.withValues(alpha: 0.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withValues(alpha: 0.4),
|
||||
blurRadius: 40,
|
||||
blurRadius: Responsive.scale(context, 40),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GlassContainer(
|
||||
borderRadius: 50,
|
||||
borderRadius: Responsive.scale(context, 50),
|
||||
blur: 10,
|
||||
color: colorScheme.surface.withValues(alpha: 0.1),
|
||||
borderOpacity: 0.2,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: Responsive.scale(context, 80),
|
||||
height: Responsive.scale(context, 80),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
@@ -199,7 +210,7 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
),
|
||||
child: Icon(
|
||||
permission.icon,
|
||||
size: 36,
|
||||
size: Responsive.scale(context, 36),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
@@ -208,32 +219,35 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionCard(BuildContext context, FlatpakPermission permission) {
|
||||
Widget _buildDescriptionCard(
|
||||
BuildContext context,
|
||||
FlatpakPermission permission,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return GlassContainer(
|
||||
borderRadius: 16,
|
||||
borderRadius: Responsive.scale(context, 16),
|
||||
blur: 0,
|
||||
color: isDark
|
||||
? Colors.white.withValues(alpha: 0.05)
|
||||
: Colors.black.withValues(alpha: 0.03),
|
||||
borderOpacity: 0.1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: Responsive.paddingAll(context, 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 20,
|
||||
size: Responsive.scale(context, 20),
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Responsive.hGap(context, 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
permission.description,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontSize: Responsive.fontSize(context, 13),
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
height: 1.4,
|
||||
),
|
||||
@@ -245,7 +259,10 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressIndicator(ColorScheme colorScheme) {
|
||||
Widget _buildProgressIndicator(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
final progress = widget.event.progress!;
|
||||
final total = widget.event.total!;
|
||||
|
||||
@@ -257,7 +274,7 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
Text(
|
||||
'Permission $progress of $total',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: Responsive.fontSize(context, 12),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.primary,
|
||||
letterSpacing: 0.5,
|
||||
@@ -265,11 +282,11 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Responsive.vGap(context, 12),
|
||||
Container(
|
||||
height: 6,
|
||||
height: Responsive.scale(context, 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(Responsive.scale(context, 10)),
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.1),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
@@ -277,7 +294,9 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
widthFactor: progress / total,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(
|
||||
Responsive.scale(context, 10),
|
||||
),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
colorScheme.primary.withValues(alpha: 0.7),
|
||||
@@ -287,8 +306,8 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withValues(alpha: 0.4),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
blurRadius: Responsive.scale(context, 6),
|
||||
offset: Offset(0, Responsive.scale(context, 2)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -299,7 +318,11 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(ColorScheme colorScheme, bool isDark) {
|
||||
Widget _buildActionButtons(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
bool isDark,
|
||||
) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -317,7 +340,7 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
Responsive.hGap(context, 16),
|
||||
|
||||
Expanded(
|
||||
child: GlassButton(
|
||||
@@ -344,11 +367,13 @@ class _PermissionDialogContentState extends State<_PermissionDialogContent>
|
||||
final name = parts.last;
|
||||
return name
|
||||
.split('_')
|
||||
.map((word) => word.isNotEmpty
|
||||
? word[0].toUpperCase() + word.substring(1)
|
||||
: '')
|
||||
.map(
|
||||
(word) => word.isNotEmpty
|
||||
? word[0].toUpperCase() + word.substring(1)
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
return appId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user