mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Implementing scroll bar like Google Photos
This commit is contained in:
		
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
run_server_dev:
 | 
			
		||||
	docker-compose -f ./server/docker-compose.yml up
 | 
			
		||||
 | 
			
		||||
run_server_update:
 | 
			
		||||
	docker-compose -f ./server/docker-compose.yml up --build -V
 | 
			
		||||
@@ -27,13 +27,13 @@ Then populate the value in there.
 | 
			
		||||
To start, run
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
docker-compose up ./server
 | 
			
		||||
docker-compose -f ./server/docker-compose.yml up
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
To force rebuild node modules after installing new packages
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
docker-compose up --build -V ./server
 | 
			
		||||
docker-compose -f ./server/docker-compose.yml up --build -V
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Known Issue
 | 
			
		||||
 
 | 
			
		||||
@@ -46,10 +46,6 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> initApp() async {
 | 
			
		||||
    // ! TOBE DELETE
 | 
			
		||||
    // Simulate Sign In And Register/Get Device ID
 | 
			
		||||
    // await ref.read(authenticationProvider.notifier).login();
 | 
			
		||||
    // ref.read(backupProvider.notifier).getBackupInfo();
 | 
			
		||||
    // WidgetsBinding.instance?.addObserver(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,12 +75,14 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
                onPressed: () async {
 | 
			
		||||
                  var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
 | 
			
		||||
 | 
			
		||||
                  // Fetch new image
 | 
			
		||||
                  if (onPop == true) {
 | 
			
		||||
                    // Remove and force getting new widget again
 | 
			
		||||
                    if (imageGridGroup.isNotEmpty) {
 | 
			
		||||
                    // Remove and force getting new widget again if there is not many widget on screen.
 | 
			
		||||
                    // Otherwise do nothing.
 | 
			
		||||
                    if (imageGridGroup.isNotEmpty && imageGridGroup.length < 20) {
 | 
			
		||||
                      print("Get more access");
 | 
			
		||||
                      ref.read(assetProvider.notifier).getMoreAsset();
 | 
			
		||||
                    } else {
 | 
			
		||||
                    } else if (imageGridGroup.isEmpty) {
 | 
			
		||||
                      print("get immich asset");
 | 
			
		||||
                      ref.read(assetProvider.notifier).getImmichAssets();
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/rendering.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
 | 
			
		||||
@@ -8,6 +9,7 @@ import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.da
 | 
			
		||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
 | 
			
		||||
import 'package:visibility_detector/visibility_detector.dart';
 | 
			
		||||
import 'package:intl/intl.dart';
 | 
			
		||||
 | 
			
		||||
class HomePage extends HookConsumerWidget {
 | 
			
		||||
@@ -18,10 +20,9 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
    final ValueNotifier<bool> _showBackToTopBtn = useState(false);
 | 
			
		||||
    ScrollController _scrollController = useScrollController();
 | 
			
		||||
    List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider);
 | 
			
		||||
    BackUpState _backupState = ref.watch(backupProvider);
 | 
			
		||||
    List<Widget> imageGridGroup = [];
 | 
			
		||||
    List<GlobalKey> monthGroupKey = [];
 | 
			
		||||
 | 
			
		||||
    final monthInView = useState<String>("");
 | 
			
		||||
    _scrollControllerCallback() {
 | 
			
		||||
      var endOfPage = _scrollController.position.maxScrollExtent;
 | 
			
		||||
 | 
			
		||||
@@ -34,6 +35,13 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
      } else {
 | 
			
		||||
        _showBackToTopBtn.value = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Quick Scroll For Jumping to Month
 | 
			
		||||
      if (_scrollController.position.userScrollDirection == ScrollDirection.forward) {
 | 
			
		||||
        // Scroll UP
 | 
			
		||||
      } else if (_scrollController.position.userScrollDirection == ScrollDirection.reverse) {
 | 
			
		||||
        // SCroll Down
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
@@ -41,8 +49,63 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
      _scrollController.addListener(_scrollControllerCallback);
 | 
			
		||||
 | 
			
		||||
      return () => _scrollController.removeListener(_scrollControllerCallback);
 | 
			
		||||
    }, [_scrollController, key]);
 | 
			
		||||
      return () {
 | 
			
		||||
        debugPrint("Remove scroll listener");
 | 
			
		||||
        _scrollController.removeListener(_scrollControllerCallback);
 | 
			
		||||
      };
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    SliverToBoxAdapter _buildMonthGroupTitle(String dateTitle, BuildContext context) {
 | 
			
		||||
      return SliverToBoxAdapter(
 | 
			
		||||
        child: Padding(
 | 
			
		||||
          padding: const EdgeInsets.only(left: 10.0, top: 32),
 | 
			
		||||
          child: Text(
 | 
			
		||||
            DateFormat('MMMM, y').format(
 | 
			
		||||
              DateTime.parse(dateTitle),
 | 
			
		||||
            ),
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
              fontSize: 24,
 | 
			
		||||
              fontWeight: FontWeight.bold,
 | 
			
		||||
              color: Theme.of(context).primaryColor,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    SliverToBoxAdapter _buildDateGroupTitle(String dateTitle) {
 | 
			
		||||
      var currentYear = DateTime.now().year;
 | 
			
		||||
      var groupYear = DateTime.parse(dateTitle).year;
 | 
			
		||||
      var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
 | 
			
		||||
      var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(dateTitle));
 | 
			
		||||
      var monthText = DateFormat('MMMM, y').format(DateTime.parse(dateTitle));
 | 
			
		||||
      return SliverToBoxAdapter(
 | 
			
		||||
        child: VisibilityDetector(
 | 
			
		||||
          key: Key(dateText),
 | 
			
		||||
          onVisibilityChanged: (visibilityInfo) {
 | 
			
		||||
            monthInView.value = monthText;
 | 
			
		||||
          },
 | 
			
		||||
          child: Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(top: 24.0, bottom: 24.0, left: 3.0),
 | 
			
		||||
            child: Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(left: 8.0, bottom: 5.0, top: 5.0),
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    dateText,
 | 
			
		||||
                    style: const TextStyle(
 | 
			
		||||
                      fontSize: 14,
 | 
			
		||||
                      fontWeight: FontWeight.bold,
 | 
			
		||||
                      color: Colors.black87,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Widget _buildBody() {
 | 
			
		||||
      if (assetGroup.isNotEmpty) {
 | 
			
		||||
@@ -56,28 +119,9 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
          int? previousMonth = DateTime.tryParse(lastGroupDate)?.month;
 | 
			
		||||
 | 
			
		||||
          if ((currentMonth! - previousMonth!) != 0) {
 | 
			
		||||
            var myKey = GlobalKey();
 | 
			
		||||
            monthGroupKey.add(myKey);
 | 
			
		||||
            // debugPrint("Group Key $myKey");
 | 
			
		||||
            var monthTitleText = DateFormat('MMMM, y').format(DateTime.parse(dateTitle));
 | 
			
		||||
 | 
			
		||||
            imageGridGroup.add(
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                key: myKey,
 | 
			
		||||
                child: Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(left: 10.0, top: 32),
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    DateFormat('MMMM, y').format(
 | 
			
		||||
                      DateTime.parse(dateTitle),
 | 
			
		||||
                    ),
 | 
			
		||||
                    style: TextStyle(
 | 
			
		||||
                      fontSize: 24,
 | 
			
		||||
                      fontWeight: FontWeight.bold,
 | 
			
		||||
                      color: Theme.of(context).primaryColor,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
            imageGridGroup.add(_buildMonthGroupTitle(monthTitleText, context));
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          imageGridGroup.add(
 | 
			
		||||
@@ -90,37 +134,34 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
       return SafeArea(
 | 
			
		||||
          child: CustomScrollView(
 | 
			
		||||
      return SafeArea(
 | 
			
		||||
        child: Stack(children: [
 | 
			
		||||
          RawScrollbar(
 | 
			
		||||
            minThumbLength: 50,
 | 
			
		||||
            isAlwaysShown: false,
 | 
			
		||||
            interactive: true,
 | 
			
		||||
            controller: _scrollController,
 | 
			
		||||
            slivers: [
 | 
			
		||||
              ImmichSliverAppBar(imageGridGroup: imageGridGroup),
 | 
			
		||||
              ...imageGridGroup,
 | 
			
		||||
            ],
 | 
			
		||||
            thickness: 50,
 | 
			
		||||
            crossAxisMargin: -20,
 | 
			
		||||
            mainAxisMargin: 70,
 | 
			
		||||
            timeToFade: const Duration(seconds: 2),
 | 
			
		||||
            thumbColor: Colors.blueGrey,
 | 
			
		||||
            radius: const Radius.circular(30),
 | 
			
		||||
            child: CustomScrollView(
 | 
			
		||||
              controller: _scrollController,
 | 
			
		||||
              slivers: [
 | 
			
		||||
                ImmichSliverAppBar(imageGridGroup: imageGridGroup),
 | 
			
		||||
                ...imageGridGroup,
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
        ]),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      drawer: const ProfileDrawer(),
 | 
			
		||||
      body: _buildBody(),
 | 
			
		||||
      bottomNavigationBar: BottomAppBar(
 | 
			
		||||
        child: IconButton(
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            if (monthGroupKey.isNotEmpty) {
 | 
			
		||||
              var targetContext = monthGroupKey.last.currentContext;
 | 
			
		||||
              if (targetContext != null) {
 | 
			
		||||
                Scrollable.ensureVisible(
 | 
			
		||||
                  targetContext,
 | 
			
		||||
                  duration: const Duration(milliseconds: 400),
 | 
			
		||||
                  curve: Curves.easeInOut,
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          icon: const Icon(Icons.ac_unit_outlined),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      floatingActionButton: _showBackToTopBtn.value
 | 
			
		||||
          ? FloatingActionButton.small(
 | 
			
		||||
              enableFeedback: true,
 | 
			
		||||
@@ -134,30 +175,4 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SliverToBoxAdapter _buildDateGroupTitle(String dateTitle) {
 | 
			
		||||
    var currentYear = DateTime.now().year;
 | 
			
		||||
    var groupYear = DateTime.parse(dateTitle).year;
 | 
			
		||||
    var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
 | 
			
		||||
    return SliverToBoxAdapter(
 | 
			
		||||
      child: Padding(
 | 
			
		||||
        padding: const EdgeInsets.only(top: 24.0, bottom: 24.0, left: 3.0),
 | 
			
		||||
        child: Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(left: 8.0, bottom: 5.0, top: 5.0),
 | 
			
		||||
              child: Text(
 | 
			
		||||
                DateFormat(formatDateTemplate).format(DateTime.parse(dateTitle)),
 | 
			
		||||
                style: const TextStyle(
 | 
			
		||||
                  fontSize: 14,
 | 
			
		||||
                  fontWeight: FontWeight.bold,
 | 
			
		||||
                  color: Colors.black87,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ class LoginForm extends HookConsumerWidget {
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final usernameController = useTextEditingController(text: 'testuser@email.com');
 | 
			
		||||
    final passwordController = useTextEditingController(text: 'password');
 | 
			
		||||
    final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216');
 | 
			
		||||
    final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:3000');
 | 
			
		||||
 | 
			
		||||
    return Center(
 | 
			
		||||
      child: ConstrainedBox(
 | 
			
		||||
 
 | 
			
		||||
@@ -805,6 +805,13 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.1"
 | 
			
		||||
  visibility_detector:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: visibility_detector
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.2.2"
 | 
			
		||||
  watcher:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,8 @@ dependencies:
 | 
			
		||||
  auto_route: ^3.2.2
 | 
			
		||||
  exif: ^3.1.1
 | 
			
		||||
  transparent_image: ^2.0.0
 | 
			
		||||
  visibility_detector: ^0.2.2
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
    sdk: flutter
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,7 @@ export class AssetService {
 | 
			
		||||
          lastQueryCreatedAt: query.nextPageKey || new Date().toISOString(),
 | 
			
		||||
        })
 | 
			
		||||
        .orderBy('a."createdAt"::date', 'DESC')
 | 
			
		||||
        .take(200)
 | 
			
		||||
        .take(10000)
 | 
			
		||||
        .getMany();
 | 
			
		||||
 | 
			
		||||
      if (assets.length > 0) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user