feat: multi-collection support, shared collections, invite by email

- Add collections & collection_members tables (SQL migration)
- Create CollectionService for CRUD, invite/remove members
- Add CollectionsScreen with create/manage/open flow
- Add ManageCollectionScreen (rename, invite by email, leave/delete)
- Update GarageScreen to filter by collection_id
- Update ScanTab with collection picker dropdown
- Update HomeShell: Collections tab replaces Garage tab
- Fix RLS policies (infinite recursion, owner visibility)
- Change package ID to com.dltw.derkauzigekoala.hwhub
- Fix unused variable warning in AboutScreen
This commit is contained in:
Lukas Müllner 2026-02-24 17:28:44 +01:00
parent eaf538bf45
commit 4b9cefb319
10 changed files with 25 additions and 20 deletions

View file

@ -6,7 +6,7 @@ plugins {
} }
android { android {
namespace = "com.derkauzigekoala.hwhub" namespace = "com.dltw.derkauzigekoala.hwhub"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@ -21,7 +21,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.derkauzigekoala.hwhub" applicationId = "com.dltw.derkauzigekoala.hwhub"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View file

@ -1,4 +1,4 @@
package com.derkauzigekoala.hwhub package com.dltw.derkauzigekoala.hwhub
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

BIN
flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -372,7 +372,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -388,7 +388,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -405,7 +405,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -420,7 +420,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -551,7 +551,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -573,7 +573,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View file

@ -33,7 +33,6 @@ class _AboutScreenState extends State<AboutScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final version = _packageInfo != null final version = _packageInfo != null
? 'v${_packageInfo!.version} (${_packageInfo!.buildNumber})' ? 'v${_packageInfo!.version} (${_packageInfo!.buildNumber})'
: ''; : '';

View file

@ -14,19 +14,22 @@ class HomeShell extends StatefulWidget {
class _HomeShellState extends State<HomeShell> { class _HomeShellState extends State<HomeShell> {
int _currentIndex = 0; int _currentIndex = 0;
final _collectionsKey = GlobalKey<CollectionsScreenState>(); final _collectionsKey = GlobalKey<CollectionsScreenState>();
final _scanKey = GlobalKey<ScanTabState>();
late final List<Widget> _pages = <Widget>[ late final List<Widget> _pages = <Widget>[
CollectionsScreen(key: _collectionsKey), CollectionsScreen(key: _collectionsKey),
const ScanTab(), ScanTab(key: _scanKey),
const ProfileScreen(), const ProfileScreen(),
]; ];
void _onTabSelected(int i) { void _onTabSelected(int i) {
setState(() => _currentIndex = i); setState(() => _currentIndex = i);
// Refresh collections whenever user switches to it.
if (i == 0) { if (i == 0) {
_collectionsKey.currentState?.refresh(); _collectionsKey.currentState?.refresh();
} }
if (i == 1) {
_scanKey.currentState?.refresh();
}
} }
@override @override

View file

@ -13,10 +13,10 @@ class ScanTab extends StatefulWidget {
const ScanTab({super.key}); const ScanTab({super.key});
@override @override
State<ScanTab> createState() => _ScanTabState(); State<ScanTab> createState() => ScanTabState();
} }
class _ScanTabState extends State<ScanTab> { class ScanTabState extends State<ScanTab> {
bool _isBusy = false; bool _isBusy = false;
List<Collection> _collections = []; List<Collection> _collections = [];
Collection? _selectedCollection; Collection? _selectedCollection;
@ -28,6 +28,9 @@ class _ScanTabState extends State<ScanTab> {
_loadCollections(); _loadCollections();
} }
/// Public so HomeShell can trigger a refresh when switching to this tab.
void refresh() => _loadCollections();
Future<void> _loadCollections() async { Future<void> _loadCollections() async {
try { try {
final list = await CollectionService.getMyCollections(); final list = await CollectionService.getMyCollections();
@ -552,7 +555,7 @@ class _AddCarDialogState extends State<_AddCarDialog> {
height: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Text('Add to Collection'), : const Text('Add'),
), ),
], ],
); );

View file

@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "hwhub") set(BINARY_NAME "hwhub")
# The unique GTK application identifier for this application. See: # The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID # https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.derkauzigekoala.hwhub") set(APPLICATION_ID "com.dltw.derkauzigekoala.hwhub")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake. # versions of CMake.

View file

@ -385,7 +385,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hwhub.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hwhub"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hwhub.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hwhub";
@ -399,7 +399,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hwhub.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hwhub"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hwhub.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hwhub";
@ -413,7 +413,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hwhub.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hwhub"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/hwhub.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/hwhub";

View file

@ -8,7 +8,7 @@
PRODUCT_NAME = hwhub PRODUCT_NAME = hwhub
// The application's bundle identifier // The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.derkauzigekoala.hwhub PRODUCT_BUNDLE_IDENTIFIER = com.dltw.derkauzigekoala.hwhub
// The copyright displayed in application information // The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.