Merge pull request #74 from AhmedAdelWafdy7/flatpak/responsive

flatpak: fix UI Responsiveness and handle storage
This commit is contained in:
Joel Winarske
2026-04-08 08:57:24 -07:00
committed by GitHub
29 changed files with 1221 additions and 883 deletions
@@ -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:
+2 -1
View File
@@ -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