diff --git a/.gitignore b/.gitignore
index e2039378a..a69a7ad65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,4 +22,5 @@ uni/android/key.properties
**.jks
# Custom Lint log
-custom_lint.log
\ No newline at end of file
+custom_lint.log
+.direnv
diff --git a/flake.lock b/flake.lock
index 2f3c7570e..a57a81f83 100644
--- a/flake.lock
+++ b/flake.lock
@@ -54,11 +54,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1770562336,
- "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
+ "lastModified": 1773579282,
+ "narHash": "sha256-LWvZj9Bvm1EuoO6zbX4yjZebwnZNfeTbmCJGS7RGQ3Y=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
+ "rev": "5a88de74db0e948139be4b46f9a94d64aa11391c",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index e36417391..6c5e9099f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -48,7 +48,7 @@
ndkVersions = ["28.2.13676358"];
};
- flutter = pkgs.flutter338;
+ flutter = pkgs.flutter;
jdks = with pkgs; [jdk21 jdk17];
};
} {
diff --git a/packages/uni_app/android/app/src/main/AndroidManifest.xml b/packages/uni_app/android/app/src/main/AndroidManifest.xml
index 618033b6e..57a58da12 100644
--- a/packages/uni_app/android/app/src/main/AndroidManifest.xml
+++ b/packages/uni_app/android/app/src/main/AndroidManifest.xml
@@ -7,6 +7,23 @@
android:usesCleartextTraffic="true"
android:allowBackup="false"
>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/uni_app/android/app/src/main/res/xml/automotive_app_desc.xml b/packages/uni_app/android/app/src/main/res/xml/automotive_app_desc.xml
new file mode 100644
index 000000000..8dc476fe0
--- /dev/null
+++ b/packages/uni_app/android/app/src/main/res/xml/automotive_app_desc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj b/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj
index 9f321be0b..d5a117f42 100644
--- a/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/uni_app/ios/Runner.xcodeproj/project.pbxproj
@@ -16,6 +16,7 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A19789621A76D32B94690770 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65A7FEFFF94135D00ABAE9B9 /* Pods_Runner.framework */; };
B0BA8183595B62BB34BA7067 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D20E49018C2538464D8BC7C /* Pods_RunnerTests.framework */; };
+ C14713CF2F68ABEF00644B7E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14713CE2F68ABEF00644B7E /* SceneDelegate.swift */; };
C183B2E32ED222610052FE14 /* uni.icon in Resources */ = {isa = PBXBuildFile; fileRef = C183B2E22ED222610052FE14 /* uni.icon */; };
C183B2E52ED222700052FE14 /* uni_dev.icon in Resources */ = {isa = PBXBuildFile; fileRef = C183B2E42ED222700052FE14 /* uni_dev.icon */; };
/* End PBXBuildFile section */
@@ -67,6 +68,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ C14713CE2F68ABEF00644B7E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ C14713D02F68AE1E00644B7E /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
C183B2E22ED222610052FE14 /* uni.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = uni.icon; sourceTree = ""; };
C183B2E42ED222700052FE14 /* uni_dev.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = uni_dev.icon; sourceTree = ""; };
/* End PBXFileReference section */
@@ -147,6 +150,8 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
+ C14713D02F68AE1E00644B7E /* Runner.entitlements */,
+ C14713CE2F68ABEF00644B7E /* SceneDelegate.swift */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -386,6 +391,7 @@
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ C14713CF2F68ABEF00644B7E /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -478,6 +484,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = uni;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -701,6 +708,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = uni_dev;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -724,6 +732,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = uni;
CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/packages/uni_app/ios/Runner/AppDelegate.swift b/packages/uni_app/ios/Runner/AppDelegate.swift
index df8182fe6..5e496a71b 100644
--- a/packages/uni_app/ios/Runner/AppDelegate.swift
+++ b/packages/uni_app/ios/Runner/AppDelegate.swift
@@ -4,13 +4,17 @@ import workmanager
import flutter_local_notifications
import app_links
+// Global Flutter engine for sharing between app and CarPlay
+let flutterEngine = FlutterEngine(name: "SharedEngine", project: nil, allowHeadlessExecution: true)
+
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
+ flutterEngine.run()
+ GeneratedPluginRegistrant.register(with: flutterEngine)
// Notifications
WorkmanagerPlugin.registerTask(withIdentifier:"pt.up.fe.ni.uni.notificationworker")
diff --git a/packages/uni_app/ios/Runner/Info.plist b/packages/uni_app/ios/Runner/Info.plist
index e3c79a662..d8261ff6e 100644
--- a/packages/uni_app/ios/Runner/Info.plist
+++ b/packages/uni_app/ios/Runner/Info.plist
@@ -61,8 +61,31 @@
Possibilidade de adicionar prints a um bug report
UIApplicationSceneManifest
+ UIApplicationSupportsMultipleScenes
+
UISceneConfigurations
-
+
+ CPTemplateApplicationSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ CarPlay Configuration
+ UISceneDelegateClassName
+ flutter_carplay.FlutterCarPlaySceneDelegate
+
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
UIApplicationSupportsIndirectInputEvents
diff --git a/packages/uni_app/ios/Runner/Runner.entitlements b/packages/uni_app/ios/Runner/Runner.entitlements
new file mode 100644
index 000000000..aba04fc97
--- /dev/null
+++ b/packages/uni_app/ios/Runner/Runner.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.developer.carplay-parking
+
+
+
\ No newline at end of file
diff --git a/packages/uni_app/ios/Runner/SceneDelegate.swift b/packages/uni_app/ios/Runner/SceneDelegate.swift
new file mode 100644
index 000000000..8e41f9548
--- /dev/null
+++ b/packages/uni_app/ios/Runner/SceneDelegate.swift
@@ -0,0 +1,25 @@
+import UIKit
+import Flutter
+
+@available(iOS 13.0, *)
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+ var window: UIWindow?
+
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+ guard let windowScene = scene as? UIWindowScene else { return }
+
+ window = UIWindow(windowScene: windowScene)
+
+ let controller = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
+
+ // Attach the native Launch Screen to hide the black screen transition
+ if let storyboardName = Bundle.main.object(forInfoDictionaryKey: "UILaunchStoryboardName") as? String,
+ let launchScreen = UIStoryboard(name: storyboardName, bundle: nil).instantiateInitialViewController()?.view {
+ launchScreen.frame = UIScreen.main.bounds
+ controller.splashScreenView = launchScreen
+ }
+
+ window?.rootViewController = controller
+ window?.makeKeyAndVisible()
+ }
+}
\ No newline at end of file
diff --git a/packages/uni_app/lib/controller/car_controller.dart b/packages/uni_app/lib/controller/car_controller.dart
new file mode 100644
index 000000000..549c7421f
--- /dev/null
+++ b/packages/uni_app/lib/controller/car_controller.dart
@@ -0,0 +1,91 @@
+import 'dart:io';
+import 'package:flutter_carplay/flutter_carplay.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:uni/generated/l10n.dart';
+import 'package:uni/model/entities/parking_lot_occupation.dart';
+import 'package:uni/model/providers/riverpod/parking_lot_provider.dart';
+
+class CarController {
+ CarController(this.ref);
+
+ final Ref ref;
+ bool _initialized = false;
+
+ void init() {
+ if (_initialized) {
+ return;
+ }
+ _initialized = true;
+
+ ref.listen(parkingLotProvider, (previous, next) {
+ next.whenData((occupation) {
+ if (occupation != null) {
+ _updateCarTemplates(occupation);
+ }
+ });
+ }, fireImmediately: true);
+ }
+
+ void _updateCarTemplates(ParkingLotOccupation occupation) {
+ S s;
+ try {
+ s = S.current;
+ } catch (_) {
+ return;
+ }
+
+ if (Platform.isIOS) {
+ final cpItems = occupation.lots.map((lot) {
+ final name = _lotName(lot.type, s);
+ final free = lot.free;
+ final capacity = lot.capacity;
+ return CPListItem(
+ text: name,
+ detailText: '$free / $capacity ${s.parking_lot_free}',
+ onPress: (complete, self) => complete(),
+ );
+ }).toList();
+
+ FlutterCarplay.setRootTemplate(
+ rootTemplate: CPListTemplate(
+ sections: [CPListSection(items: cpItems, header: s.parking_lots)],
+ title: 'uni',
+ systemIcon: 'car',
+ ),
+ );
+ }
+
+ if (Platform.isAndroid) {
+ final aaItems = occupation.lots.map((lot) {
+ final name = _lotName(lot.type, s);
+ final free = lot.free;
+ final capacity = lot.capacity;
+ return AAListItem(
+ title: name,
+ subtitle: '$free / $capacity ${s.parking_lot_free}',
+ onPress: (complete, item) => complete(),
+ );
+ }).toList();
+
+ FlutterAndroidAuto.setRootTemplate(
+ template: AAListTemplate(
+ sections: [AAListSection(items: aaItems, title: s.parking_lots)],
+ title: 'uni',
+ ),
+ );
+ }
+ }
+
+ String _lotName(ParkingLotType type, S s) {
+ switch (type) {
+ case ParkingLotType.permanentStaff:
+ return s.parking_lot_permanent_staff;
+ case ParkingLotType.students:
+ return s.parking_lot_students;
+ case ParkingLotType.nonPermanentStaff:
+ return s.parking_lot_non_permanent_staff;
+ }
+ }
+}
+
+final carControllerProvider = Provider(CarController.new);
diff --git a/packages/uni_app/lib/controller/fetchers/parking_lot_fetcher.dart b/packages/uni_app/lib/controller/fetchers/parking_lot_fetcher.dart
new file mode 100644
index 000000000..ec2f062ae
--- /dev/null
+++ b/packages/uni_app/lib/controller/fetchers/parking_lot_fetcher.dart
@@ -0,0 +1,50 @@
+import 'dart:convert';
+
+import 'package:uni/controller/networking/network_router.dart';
+import 'package:uni/model/entities/parking_lot_occupation.dart';
+import 'package:uni/session/flows/base/session.dart';
+
+class ParkingLotFetcher {
+ static const _endpoint = 'instalacs_geral.ocupacao_parques';
+
+ Future getParkingLotOccupation(Session session) async {
+ final url = '${NetworkRouter.getBaseUrl('feup')}$_endpoint';
+
+ final response = await NetworkRouter.getWithCookies(url, {}, session);
+ return _parse(response.body);
+ }
+
+ ParkingLotOccupation _parse(String body) {
+ final json = jsonDecode(body) as Map;
+ final itdc = json['itdc'] as List;
+ final resposta =
+ (itdc.first as Map)['resposta']
+ as Map;
+
+ final lots = [
+ ParkingLot(
+ id: 'P1',
+ type: ParkingLotType.permanentStaff,
+ capacity: resposta['p1lotacao'] as int,
+ occupied: resposta['p1ocupados'] as int,
+ free: resposta['p1livres'] as int,
+ ),
+ ParkingLot(
+ id: 'P3',
+ type: ParkingLotType.students,
+ capacity: resposta['p3lotacao'] as int,
+ occupied: resposta['p3ocupados'] as int,
+ free: resposta['p3livres'] as int,
+ ),
+ ParkingLot(
+ id: 'P4',
+ type: ParkingLotType.nonPermanentStaff,
+ capacity: resposta['p4lotacao'] as int,
+ occupied: resposta['p4ocupados'] as int,
+ free: resposta['p4livres'] as int,
+ ),
+ ];
+
+ return ParkingLotOccupation(lots);
+ }
+}
diff --git a/packages/uni_app/lib/generated/intl/messages_en.dart b/packages/uni_app/lib/generated/intl/messages_en.dart
index 015247fa8..b3a25d615 100644
--- a/packages/uni_app/lib/generated/intl/messages_en.dart
+++ b/packages/uni_app/lib/generated/intl/messages_en.dart
@@ -334,6 +334,17 @@ class MessageLookup extends MessageLookupByLibrary {
"no_library_info": MessageLookupByLibrary.simpleMessage(
"No library occupation information available",
),
+ "no_parking_info": MessageLookupByLibrary.simpleMessage(
+ "No parking lot information available",
+ ),
+ "parking_lot_free": MessageLookupByLibrary.simpleMessage("free"),
+ "parking_lot_non_permanent_staff": MessageLookupByLibrary.simpleMessage(
+ "Non-Permanent Staff",
+ ),
+ "parking_lot_permanent_staff": MessageLookupByLibrary.simpleMessage(
+ "Permanent Staff",
+ ),
+ "parking_lot_students": MessageLookupByLibrary.simpleMessage("Students"),
"no_link": MessageLookupByLibrary.simpleMessage(
"We couldn\'t open the link",
),
@@ -383,6 +394,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Error opening the file",
),
"other_links": MessageLookupByLibrary.simpleMessage("Other links"),
+ "parking": MessageLookupByLibrary.simpleMessage("Parking"),
+ "parking_lots": MessageLookupByLibrary.simpleMessage("Parking Lots"),
"pass_change_request": MessageLookupByLibrary.simpleMessage(
"For security reasons, passwords must be changed periodically.",
),
diff --git a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart
index bedaae31e..9a9fb0c4f 100644
--- a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart
+++ b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart
@@ -354,6 +354,17 @@ class MessageLookup extends MessageLookupByLibrary {
"no_library_info": MessageLookupByLibrary.simpleMessage(
"Sem informação de ocupação",
),
+ "no_parking_info": MessageLookupByLibrary.simpleMessage(
+ "Sem informação de estacionamento disponível",
+ ),
+ "parking_lot_free": MessageLookupByLibrary.simpleMessage("livre"),
+ "parking_lot_non_permanent_staff": MessageLookupByLibrary.simpleMessage(
+ "Pessoal Não Permanente",
+ ),
+ "parking_lot_permanent_staff": MessageLookupByLibrary.simpleMessage(
+ "Pessoal Permanente",
+ ),
+ "parking_lot_students": MessageLookupByLibrary.simpleMessage("Estudantes"),
"no_link": MessageLookupByLibrary.simpleMessage(
"Não conseguimos abrir o link",
),
@@ -405,6 +416,10 @@ class MessageLookup extends MessageLookupByLibrary {
"Erro ao abrir o ficheiro",
),
"other_links": MessageLookupByLibrary.simpleMessage("Outros links"),
+ "parking": MessageLookupByLibrary.simpleMessage("Estacionamento"),
+ "parking_lots": MessageLookupByLibrary.simpleMessage(
+ "Parques de Estacionamento",
+ ),
"pass_change_request": MessageLookupByLibrary.simpleMessage(
"Por razões de segurança, as palavras-passe têm de ser alteradas periodicamente.",
),
diff --git a/packages/uni_app/lib/generated/l10n.dart b/packages/uni_app/lib/generated/l10n.dart
index 8928735f9..c89eaf22a 100644
--- a/packages/uni_app/lib/generated/l10n.dart
+++ b/packages/uni_app/lib/generated/l10n.dart
@@ -722,6 +722,66 @@ class S {
);
}
+ /// `Parking Lots`
+ String get parking_lots {
+ return Intl.message(
+ 'Parking Lots',
+ name: 'parking_lots',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Parking`
+ String get parking {
+ return Intl.message('Parking', name: 'parking', desc: '', args: []);
+ }
+
+ /// `No parking lot information available`
+ String get no_parking_info {
+ return Intl.message(
+ 'No parking lot information available',
+ name: 'no_parking_info',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Permanent Staff`
+ String get parking_lot_permanent_staff {
+ return Intl.message(
+ 'Permanent Staff',
+ name: 'parking_lot_permanent_staff',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Students`
+ String get parking_lot_students {
+ return Intl.message(
+ 'Students',
+ name: 'parking_lot_students',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `Non-Permanent Staff`
+ String get parking_lot_non_permanent_staff {
+ return Intl.message(
+ 'Non-Permanent Staff',
+ name: 'parking_lot_non_permanent_staff',
+ desc: '',
+ args: [],
+ );
+ }
+
+ /// `free`
+ String get parking_lot_free {
+ return Intl.message('free', name: 'parking_lot_free', desc: '', args: []);
+ }
+
/// `Lunch`
String get lunch {
return Intl.message('Lunch', name: 'lunch', desc: '', args: []);
diff --git a/packages/uni_app/lib/l10n/intl_en.arb b/packages/uni_app/lib/l10n/intl_en.arb
index b6e687820..a5539a656 100644
--- a/packages/uni_app/lib/l10n/intl_en.arb
+++ b/packages/uni_app/lib/l10n/intl_en.arb
@@ -172,6 +172,20 @@
"@leave_feedback": {},
"library_occupation": "Library Occupation",
"@library_occupation": {},
+ "parking_lots": "Parking Lots",
+ "@parking_lots": {},
+ "parking": "Parking",
+ "@parking": {},
+ "no_parking_info": "No parking lot information available",
+ "@no_parking_info": {},
+ "parking_lot_permanent_staff": "Permanent Staff",
+ "@parking_lot_permanent_staff": {},
+ "parking_lot_students": "Students",
+ "@parking_lot_students": {},
+ "parking_lot_non_permanent_staff": "Non-Permanent Staff",
+ "@parking_lot_non_permanent_staff": {},
+ "parking_lot_free": "free",
+ "@parking_lot_free": {},
"lunch": "Lunch",
"@lunch": {},
"download_error": "Error downloading the file",
diff --git a/packages/uni_app/lib/l10n/intl_pt_PT.arb b/packages/uni_app/lib/l10n/intl_pt_PT.arb
index d08c7f8a4..71dd8454c 100644
--- a/packages/uni_app/lib/l10n/intl_pt_PT.arb
+++ b/packages/uni_app/lib/l10n/intl_pt_PT.arb
@@ -176,6 +176,20 @@
"@load_error": {},
"library_occupation": "Ocupação da Biblioteca",
"@library_occupation": {},
+ "parking_lots": "Parques de Estacionamento",
+ "@parking_lots": {},
+ "parking": "Estacionamento",
+ "@parking": {},
+ "no_parking_info": "Sem informação de estacionamento disponível",
+ "@no_parking_info": {},
+ "parking_lot_permanent_staff": "Pessoal Permanente",
+ "@parking_lot_permanent_staff": {},
+ "parking_lot_students": "Estudantes",
+ "@parking_lot_students": {},
+ "parking_lot_non_permanent_staff": "Pessoal Não Permanente",
+ "@parking_lot_non_permanent_staff": {},
+ "parking_lot_free": "livre",
+ "@parking_lot_free": {},
"lunch": "Almoço",
"@lunch": {},
"download_error": "Erro ao descarregar o ficheiro",
diff --git a/packages/uni_app/lib/main.dart b/packages/uni_app/lib/main.dart
index fbefafce8..d89ba94a7 100644
--- a/packages/uni_app/lib/main.dart
+++ b/packages/uni_app/lib/main.dart
@@ -13,6 +13,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ua_client_hints/ua_client_hints.dart';
import 'package:uni/controller/background_workers/background_callback.dart';
+import 'package:uni/controller/car_controller.dart';
import 'package:uni/controller/cleanup.dart';
import 'package:uni/controller/fetchers/terms_and_conditions_fetcher.dart';
import 'package:uni/controller/local_storage/migrations/migration_controller.dart';
@@ -148,6 +149,8 @@ class ApplicationState extends ConsumerState {
if (plausible != null) {
navigatorObservers.add(PlausibleNavigatorObserver(plausible));
}
+
+ ref.read(carControllerProvider).init();
}
@override
diff --git a/packages/uni_app/lib/model/entities/parking_lot_occupation.dart b/packages/uni_app/lib/model/entities/parking_lot_occupation.dart
new file mode 100644
index 000000000..b3edb27bd
--- /dev/null
+++ b/packages/uni_app/lib/model/entities/parking_lot_occupation.dart
@@ -0,0 +1,25 @@
+enum ParkingLotType { permanentStaff, students, nonPermanentStaff }
+
+class ParkingLot {
+ ParkingLot({
+ required this.id,
+ required this.type,
+ required this.capacity,
+ required this.occupied,
+ required this.free,
+ });
+
+ final String id;
+ final ParkingLotType type;
+ final int capacity;
+ final int occupied;
+ final int free;
+
+ double get occupancyRatio => capacity > 0 ? occupied / capacity : 0.0;
+}
+
+class ParkingLotOccupation {
+ ParkingLotOccupation(this.lots);
+
+ final List lots;
+}
diff --git a/packages/uni_app/lib/model/providers/riverpod/parking_lot_provider.dart b/packages/uni_app/lib/model/providers/riverpod/parking_lot_provider.dart
new file mode 100644
index 000000000..ceda350dc
--- /dev/null
+++ b/packages/uni_app/lib/model/providers/riverpod/parking_lot_provider.dart
@@ -0,0 +1,31 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:uni/controller/fetchers/parking_lot_fetcher.dart';
+import 'package:uni/model/entities/parking_lot_occupation.dart';
+import 'package:uni/model/providers/riverpod/cached_async_notifier.dart';
+import 'package:uni/model/providers/riverpod/session_provider.dart';
+
+final parkingLotProvider =
+ AsyncNotifierProvider(
+ ParkingLotNotifier.new,
+ );
+
+final class ParkingLotNotifier
+ extends CachedAsyncNotifier {
+ @override
+ Duration? get cacheDuration => const Duration(minutes: 15);
+
+ @override
+ Future loadFromStorage() async {
+ return null;
+ }
+
+ @override
+ Future loadFromRemote() async {
+ final session = await ref.read(sessionProvider.future);
+ if (session == null) {
+ return null;
+ }
+
+ return ParkingLotFetcher().getParkingLotOccupation(session);
+ }
+}
diff --git a/packages/uni_app/lib/utils/favorite_widget_type.dart b/packages/uni_app/lib/utils/favorite_widget_type.dart
index 743d8891d..a28658de4 100644
--- a/packages/uni_app/lib/utils/favorite_widget_type.dart
+++ b/packages/uni_app/lib/utils/favorite_widget_type.dart
@@ -5,4 +5,5 @@ enum FavoriteWidgetType {
restaurants,
calendar,
news,
+ parking,
}
diff --git a/packages/uni_app/lib/view/faculty/faculty.dart b/packages/uni_app/lib/view/faculty/faculty.dart
index d427ab790..5520faab7 100644
--- a/packages/uni_app/lib/view/faculty/faculty.dart
+++ b/packages/uni_app/lib/view/faculty/faculty.dart
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uni/generated/l10n.dart';
import 'package:uni/model/providers/riverpod/library_occupation_provider.dart';
+import 'package:uni/model/providers/riverpod/parking_lot_provider.dart';
import 'package:uni/utils/navigation_items.dart';
import 'package:uni/view/faculty/widgets/service_cards.dart';
import 'package:uni/view/home/widgets/calendar/calendar_home_card.dart';
import 'package:uni/view/home/widgets/library/library_home_card.dart';
+import 'package:uni/view/home/widgets/parking/parking_home_card.dart';
import 'package:uni/view/widgets/pages_layouts/general/general.dart';
class FacultyPageView extends ConsumerStatefulWidget {
@@ -25,6 +27,10 @@ class FacultyPageViewState extends GeneralPageViewState {
return ListView(
children: const [
LibraryHomeCard(),
+ Padding(
+ padding: EdgeInsets.symmetric(vertical: 10),
+ child: ParkingLotHomeCard(),
+ ),
Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: CalendarHomeCard(),
@@ -39,6 +45,9 @@ class FacultyPageViewState extends GeneralPageViewState {
@override
Future onRefresh() async {
- await ref.read(libraryProvider.notifier).refreshRemote();
+ await Future.wait([
+ ref.read(libraryProvider.notifier).refreshRemote(),
+ ref.read(parkingLotProvider.notifier).refreshRemote(),
+ ]);
}
}
diff --git a/packages/uni_app/lib/view/home/home.dart b/packages/uni_app/lib/view/home/home.dart
index 83e34c822..f7cf6b1b9 100644
--- a/packages/uni_app/lib/view/home/home.dart
+++ b/packages/uni_app/lib/view/home/home.dart
@@ -11,6 +11,7 @@ import 'package:uni/model/providers/riverpod/exam_provider.dart';
import 'package:uni/model/providers/riverpod/lecture_provider.dart';
import 'package:uni/model/providers/riverpod/library_occupation_provider.dart';
import 'package:uni/model/providers/riverpod/news_provider.dart';
+import 'package:uni/model/providers/riverpod/parking_lot_provider.dart';
import 'package:uni/model/providers/riverpod/pedagogical_surveys_provider.dart';
import 'package:uni/model/providers/riverpod/profile_provider.dart';
import 'package:uni/model/providers/riverpod/restaurant_provider.dart';
@@ -23,6 +24,7 @@ import 'package:uni/view/home/widgets/connectivity_warning.dart';
import 'package:uni/view/home/widgets/exams/exam_home_card.dart';
import 'package:uni/view/home/widgets/library/library_home_card.dart';
import 'package:uni/view/home/widgets/news/news_home_card.dart';
+import 'package:uni/view/home/widgets/parking/parking_home_card.dart';
import 'package:uni/view/home/widgets/pedagogical_surveys_info.dart';
import 'package:uni/view/home/widgets/restaurants/restaurant_home_card.dart';
import 'package:uni/view/home/widgets/schedule/schedule_home_card.dart';
@@ -60,6 +62,7 @@ class HomePageViewState extends ConsumerState {
FavoriteWidgetType.library: libraryProvider,
FavoriteWidgetType.restaurants: restaurantProvider,
FavoriteWidgetType.news: newsProvider,
+ FavoriteWidgetType.parking: parkingLotProvider,
};
@override
@@ -104,6 +107,7 @@ class HomePageViewState extends ConsumerState {
FavoriteWidgetType.restaurants: const RestaurantHomeCard(),
FavoriteWidgetType.calendar: const CalendarHomeCard(),
FavoriteWidgetType.news: const NewsHomeCard(),
+ FavoriteWidgetType.parking: const ParkingLotHomeCard(),
};
return AnnotatedRegion(
diff --git a/packages/uni_app/lib/view/home/widgets/edit/draggable_utils.dart b/packages/uni_app/lib/view/home/widgets/edit/draggable_utils.dart
index db4b669a5..b5c64eea1 100644
--- a/packages/uni_app/lib/view/home/widgets/edit/draggable_utils.dart
+++ b/packages/uni_app/lib/view/home/widgets/edit/draggable_utils.dart
@@ -20,6 +20,8 @@ import 'package:uni_ui/icons.dart';
return (S.of(context).calendar, const UniIcon(UniIcons.calendar));
case FavoriteWidgetType.news:
return (S.of(context).news, const UniIcon(UniIcons.news));
+ case FavoriteWidgetType.parking:
+ return (S.of(context).parking, const UniIcon(UniIcons.parking));
// case 'ucs':
// title = 'UCS';
// icon = const UniIcon(UniIcons.graduationCap);
diff --git a/packages/uni_app/lib/view/home/widgets/parking/parking_card_shimmer.dart b/packages/uni_app/lib/view/home/widgets/parking/parking_card_shimmer.dart
new file mode 100644
index 000000000..00b032177
--- /dev/null
+++ b/packages/uni_app/lib/view/home/widgets/parking/parking_card_shimmer.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:shimmer/shimmer.dart';
+import 'package:uni_ui/common/generic_squircle.dart';
+
+class ShimmerParkingHomeCard extends StatelessWidget {
+ const ShimmerParkingHomeCard({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Shimmer.fromColors(
+ baseColor: Colors.grey[300]!,
+ highlightColor: Colors.grey[100]!,
+ child: GenericSquircle(
+ child: Container(
+ height: 160,
+ decoration: const BoxDecoration(color: Colors.white),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/uni_app/lib/view/home/widgets/parking/parking_home_card.dart b/packages/uni_app/lib/view/home/widgets/parking/parking_home_card.dart
new file mode 100644
index 000000000..62b63b2d3
--- /dev/null
+++ b/packages/uni_app/lib/view/home/widgets/parking/parking_home_card.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:uni/generated/l10n.dart';
+import 'package:uni/model/entities/parking_lot_occupation.dart';
+import 'package:uni/model/providers/riverpod/default_consumer.dart';
+import 'package:uni/model/providers/riverpod/parking_lot_provider.dart';
+import 'package:uni/view/home/widgets/generic_home_card.dart';
+import 'package:uni/view/home/widgets/parking/parking_card_shimmer.dart';
+import 'package:uni/view/widgets/icon_label.dart';
+import 'package:uni_ui/cards/parking_lot_card.dart';
+import 'package:uni_ui/icons.dart';
+
+String _lotName(BuildContext context, ParkingLotType type) {
+ final s = S.of(context);
+ return switch (type) {
+ ParkingLotType.permanentStaff => s.parking_lot_permanent_staff,
+ ParkingLotType.students => s.parking_lot_students,
+ ParkingLotType.nonPermanentStaff => s.parking_lot_non_permanent_staff,
+ };
+}
+
+class ParkingLotHomeCard extends GenericHomecard {
+ const ParkingLotHomeCard({super.key})
+ : super(
+ titlePadding: const EdgeInsets.symmetric(horizontal: 20),
+ bodyPadding: const EdgeInsets.symmetric(horizontal: 20),
+ );
+
+ @override
+ String getTitle(BuildContext context) {
+ return S.of(context).parking_lots;
+ }
+
+ @override
+ void onCardClick(BuildContext context) => {};
+
+ @override
+ Widget buildCardContent(BuildContext context) {
+ return DefaultConsumer(
+ provider: parkingLotProvider,
+ builder: (context, ref, occupation) => ParkingLotCard(
+ lots: occupation.lots
+ .map(
+ (lot) => ParkingLotRowWidget(
+ lotId: lot.id,
+ lotName: _lotName(context, lot.type),
+ free: lot.free,
+ capacity: lot.capacity,
+ freeLabel: S.of(context).parking_lot_free,
+ ),
+ )
+ .toList(),
+ ),
+ hasContent: (occupation) => occupation.lots.isNotEmpty,
+ nullContentWidget: Center(
+ child: IconLabel(
+ icon: const Icon(UniIcons.parking, size: 45),
+ label: S.of(context).no_parking_info,
+ labelTextStyle: TextStyle(
+ fontSize: 14,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ ),
+ loadingWidget: const ShimmerParkingHomeCard(),
+ );
+ }
+}
diff --git a/packages/uni_app/pubspec.yaml b/packages/uni_app/pubspec.yaml
index 50949efb8..d4b4b4b0e 100644
--- a/packages/uni_app/pubspec.yaml
+++ b/packages/uni_app/pubspec.yaml
@@ -37,6 +37,7 @@ dependencies:
flutter:
sdk: flutter
flutter_cache_manager: ^3.4.1
+ flutter_carplay: ^1.2.0
flutter_dotenv: ^6.0.0
flutter_local_notifications: ^20.0.0
flutter_localizations:
diff --git a/packages/uni_ui/lib/cards/parking_lot_card.dart b/packages/uni_ui/lib/cards/parking_lot_card.dart
new file mode 100644
index 000000000..68fc70837
--- /dev/null
+++ b/packages/uni_ui/lib/cards/parking_lot_card.dart
@@ -0,0 +1,89 @@
+import 'package:flutter/material.dart';
+import 'package:percent_indicator/linear_percent_indicator.dart';
+import 'package:uni_ui/cards/generic_card.dart';
+
+class ParkingLotRowWidget extends StatelessWidget {
+ const ParkingLotRowWidget({
+ super.key,
+ required this.lotId,
+ required this.lotName,
+ required this.free,
+ required this.capacity,
+ required this.freeLabel,
+ });
+
+ final String lotId;
+ final String lotName;
+ final int free;
+ final int capacity;
+ final String freeLabel;
+
+ @override
+ Widget build(BuildContext context) {
+ final occupied = capacity - free;
+ final ratio = capacity > 0 ? (occupied / capacity).clamp(0.0, 1.0) : 0.0;
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 5),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ RichText(
+ text: TextSpan(
+ children: [
+ TextSpan(
+ text: '$lotId ',
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ TextSpan(
+ text: lotName,
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ ],
+ ),
+ ),
+ Text(
+ '$free $freeLabel',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ LinearPercentIndicator(
+ lineHeight: 8.0,
+ percent: ratio,
+ backgroundColor: const Color.fromRGBO(177, 77, 84, 0.25),
+ progressColor: Theme.of(context).primaryColor,
+ barRadius: const Radius.circular(10),
+ padding: EdgeInsets.zero,
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class ParkingLotCard extends StatelessWidget {
+ const ParkingLotCard({super.key, required this.lots});
+
+ final List lots;
+
+ @override
+ Widget build(BuildContext context) {
+ return GenericCard(
+ key: key,
+ padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
+ margin: EdgeInsets.zero,
+ tooltip: '',
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: lots,
+ ),
+ );
+ }
+}
diff --git a/packages/uni_ui/lib/icons.dart b/packages/uni_ui/lib/icons.dart
index 662e5c79a..a02cda90d 100644
--- a/packages/uni_ui/lib/icons.dart
+++ b/packages/uni_ui/lib/icons.dart
@@ -94,6 +94,8 @@ class UniIcons {
static const courseUnit = PhosphorIconsDuotone.chalkboardTeacher;
static const warning = PhosphorIconsDuotone.warningOctagon;
+
+ static const parking = PhosphorIconsDuotone.garage;
}
// The same as default Icon class from material.dart but allowing to use PhosphorIcons duotone icons
diff --git a/pubspec.lock b/pubspec.lock
index a0b0d29aa..c92449fc0 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -567,6 +567,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
+ flutter_carplay:
+ dependency: transitive
+ description:
+ name: flutter_carplay
+ sha256: "134d5448a648573122af9d5a9e86c326f711150309eb518333a8bfad284604af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.11"
flutter_dotenv:
dependency: transitive
description: