Compare commits
6 Commits
pnpm
...
fix/folder
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4adf5b24f8 | ||
|
|
eca9b56847 | ||
|
|
5b0575b956 | ||
|
|
05064f87f0 | ||
|
|
522cdbac99 | ||
|
|
9240bbc6ff |
2
.github/workflows/build-mobile.yml
vendored
2
.github/workflows/build-mobile.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
|||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
|
|||||||
4
.github/workflows/cli.yml
vendored
4
.github/workflows/cli.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -76,6 +76,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -177,7 +177,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
@@ -188,6 +188,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|||||||
2
.github/workflows/prepare-release.yml
vendored
2
.github/workflows/prepare-release.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
|||||||
name: release-apk-signed
|
name: release-apk-signed
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
|
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ env.IMMICH_VERSION }}
|
tag_name: ${{ env.IMMICH_VERSION }}
|
||||||
|
|||||||
6
.github/workflows/static_analysis.yml
vendored
6
.github/workflows/static_analysis.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run DCM
|
- name: Run DCM
|
||||||
run: dcm analyze lib
|
run: dcm analyze lib --fatal-style --fatal-warnings
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
zizmor:
|
zizmor:
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -499,7 +499,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
uses: subosito/flutter-action@395322a6cded4e9ed503aebd4cc1965625f8e59a # v2.20.0
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -668,7 +668,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
|
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:1f5583fe3397210a0fbc7f11b0cec18bacc4a99e3e8ea0548e9bd6bcf26ec37a
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
|||||||
2
.github/workflows/weblate-lock.yml
vendored
2
.github/workflows/weblate-lock.yml
vendored
@@ -52,6 +52,6 @@ jobs:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ dart_code_metrics:
|
|||||||
# - no-empty-block
|
# - no-empty-block
|
||||||
# - no-equal-then-else
|
# - no-equal-then-else
|
||||||
# - prefer-correct-test-file-name
|
# - prefer-correct-test-file-name
|
||||||
|
- prefer-const-border-radius
|
||||||
# - prefer-match-file-name
|
# - prefer-match-file-name
|
||||||
# - prefer-return-await
|
# - prefer-return-await
|
||||||
# - avoid-self-assignment
|
# - avoid-self-assignment
|
||||||
@@ -290,7 +291,8 @@ dart_code_metrics:
|
|||||||
# Style
|
# Style
|
||||||
# - prefer-trailing-comma
|
# - prefer-trailing-comma
|
||||||
# - unnecessary-trailing-comma
|
# - unnecessary-trailing-comma
|
||||||
# - prefer-declaring-const-constructor
|
- prefer-declaring-const-constructor
|
||||||
# - prefer-single-widget-per-file
|
# - prefer-single-widget-per-file
|
||||||
|
- prefer-switch-expression
|
||||||
# - prefer-prefixed-global-constants
|
# - prefer-prefixed-global-constants
|
||||||
# - prefer-correct-callback-field-name
|
# - prefer-correct-callback-field-name
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'general_helper.dart';
|
|||||||
class ImmichTestLoginHelper {
|
class ImmichTestLoginHelper {
|
||||||
final WidgetTester tester;
|
final WidgetTester tester;
|
||||||
|
|
||||||
ImmichTestLoginHelper(this.tester);
|
const ImmichTestLoginHelper(this.tester);
|
||||||
|
|
||||||
Future<void> waitForLoginScreen() async {
|
Future<void> waitForLoginScreen() async {
|
||||||
await pumpUntilFound(tester, find.text("Login"));
|
await pumpUntilFound(tester, find.text("Login"));
|
||||||
@@ -60,11 +60,11 @@ class ImmichTestLoginHelper {
|
|||||||
await tester.tap(button);
|
await tester.tap(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> assertLoginSuccess({int timeoutSeconds = 15}) async {
|
Future<void> assertLoginSuccess() async {
|
||||||
await pumpUntilFound(tester, find.text("home_page_building_timeline".tr()));
|
await pumpUntilFound(tester, find.text("home_page_building_timeline".tr()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> assertLoginFailed({int timeoutSeconds = 15}) async {
|
Future<void> assertLoginFailed() async {
|
||||||
await pumpUntilFound(tester, find.text("login_form_failed_login".tr()));
|
await pumpUntilFound(tester, find.text("login_form_failed_login".tr()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ sealed class ImmichErrors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NoResponseDtoError extends ImmichErrors implements Exception {
|
class NoResponseDtoError extends ImmichErrors implements Exception {
|
||||||
|
const NoResponseDtoError();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => "Response Dto is null";
|
String toString() => "Response Dto is null";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
class Person {
|
class Person {
|
||||||
Person({
|
const Person({
|
||||||
required this.id,
|
required this.id,
|
||||||
this.birthDate,
|
this.birthDate,
|
||||||
required this.isHidden,
|
required this.isHidden,
|
||||||
|
|||||||
@@ -554,18 +554,12 @@ class Asset {
|
|||||||
}""";
|
}""";
|
||||||
}
|
}
|
||||||
|
|
||||||
static getVisibility(AssetVisibility visibility) {
|
static getVisibility(AssetVisibility visibility) => switch (visibility) {
|
||||||
switch (visibility) {
|
AssetVisibility.archive => AssetVisibilityEnum.archive,
|
||||||
case AssetVisibility.timeline:
|
AssetVisibility.hidden => AssetVisibilityEnum.hidden,
|
||||||
return AssetVisibilityEnum.timeline;
|
AssetVisibility.locked => AssetVisibilityEnum.locked,
|
||||||
case AssetVisibility.archive:
|
AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline,
|
||||||
return AssetVisibilityEnum.archive;
|
};
|
||||||
case AssetVisibility.hidden:
|
|
||||||
return AssetVisibilityEnum.hidden;
|
|
||||||
case AssetVisibility.locked:
|
|
||||||
return AssetVisibilityEnum.locked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AssetType {
|
enum AssetType {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class SSLClientCertStoreVal {
|
|||||||
final Uint8List data;
|
final Uint8List data;
|
||||||
final String? password;
|
final String? password;
|
||||||
|
|
||||||
SSLClientCertStoreVal(this.data, this.password);
|
const SSLClientCertStoreVal(this.data, this.password);
|
||||||
|
|
||||||
void save() {
|
void save() {
|
||||||
final b64Str = base64Encode(data);
|
final b64Str = base64Encode(data);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class ApiRepository {
|
|||||||
|
|
||||||
Future<T> checkNull<T>(Future<T?> future) async {
|
Future<T> checkNull<T>(Future<T?> future) async {
|
||||||
final response = await future;
|
final response = await future;
|
||||||
if (response == null) throw NoResponseDtoError();
|
if (response == null) throw const NoResponseDtoError();
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class AlbumViewerPageState {
|
|||||||
final String editTitleText;
|
final String editTitleText;
|
||||||
final String editDescriptionText;
|
final String editDescriptionText;
|
||||||
|
|
||||||
AlbumViewerPageState({
|
const AlbumViewerPageState({
|
||||||
required this.isEditAlbum,
|
required this.isEditAlbum,
|
||||||
required this.editTitleText,
|
required this.editTitleText,
|
||||||
required this.editDescriptionText,
|
required this.editDescriptionText,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
|||||||
class AssetSelectionPageResult {
|
class AssetSelectionPageResult {
|
||||||
final Set<Asset> selectedAssets;
|
final Set<Asset> selectedAssets;
|
||||||
|
|
||||||
AssetSelectionPageResult({
|
const AssetSelectionPageResult({
|
||||||
required this.selectedAssets,
|
required this.selectedAssets,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class AuthState {
|
|||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
final String profileImagePath;
|
final String profileImagePath;
|
||||||
|
|
||||||
AuthState({
|
const AuthState({
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.userEmail,
|
required this.userEmail,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class AuxilaryEndpoint {
|
|||||||
final String url;
|
final String url;
|
||||||
final AuxCheckStatus status;
|
final AuxCheckStatus status;
|
||||||
|
|
||||||
AuxilaryEndpoint({
|
const AuxilaryEndpoint({
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.status,
|
required this.status,
|
||||||
});
|
});
|
||||||
@@ -55,7 +55,7 @@ class AuxilaryEndpoint {
|
|||||||
|
|
||||||
class AuxCheckStatus {
|
class AuxCheckStatus {
|
||||||
final String name;
|
final String name;
|
||||||
AuxCheckStatus({
|
const AuxCheckStatus({
|
||||||
required this.name,
|
required this.name,
|
||||||
});
|
});
|
||||||
const AuxCheckStatus._(this.name);
|
const AuxCheckStatus._(this.name);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class LoginResponse {
|
|||||||
|
|
||||||
final String userId;
|
final String userId;
|
||||||
|
|
||||||
LoginResponse({
|
const LoginResponse({
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class AvailableAlbum {
|
|||||||
final Album album;
|
final Album album;
|
||||||
final int assetCount;
|
final int assetCount;
|
||||||
final DateTime? lastBackup;
|
final DateTime? lastBackup;
|
||||||
AvailableAlbum({
|
const AvailableAlbum({
|
||||||
required this.album,
|
required this.album,
|
||||||
required this.assetCount,
|
required this.assetCount,
|
||||||
this.lastBackup,
|
this.lastBackup,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class CurrentUploadAsset {
|
|||||||
final int? fileSize;
|
final int? fileSize;
|
||||||
final bool? iCloudAsset;
|
final bool? iCloudAsset;
|
||||||
|
|
||||||
CurrentUploadAsset({
|
const CurrentUploadAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.fileCreatedAt,
|
required this.fileCreatedAt,
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class SuccessUploadAsset {
|
|||||||
final String remoteAssetId;
|
final String remoteAssetId;
|
||||||
final bool isDuplicate;
|
final bool isDuplicate;
|
||||||
|
|
||||||
SuccessUploadAsset({
|
const SuccessUploadAsset({
|
||||||
required this.candidate,
|
required this.candidate,
|
||||||
required this.remoteAssetId,
|
required this.remoteAssetId,
|
||||||
required this.isDuplicate,
|
required this.isDuplicate,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class DownloadInfo {
|
|||||||
// enum
|
// enum
|
||||||
final TaskStatus status;
|
final TaskStatus status;
|
||||||
|
|
||||||
DownloadInfo({
|
const DownloadInfo({
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
required this.progress,
|
required this.progress,
|
||||||
required this.status,
|
required this.status,
|
||||||
@@ -71,7 +71,7 @@ class DownloadState {
|
|||||||
final TaskStatus downloadStatus;
|
final TaskStatus downloadStatus;
|
||||||
final Map<String, DownloadInfo> taskProgress;
|
final Map<String, DownloadInfo> taskProgress;
|
||||||
final bool showProgress;
|
final bool showProgress;
|
||||||
DownloadState({
|
const DownloadState({
|
||||||
required this.downloadStatus,
|
required this.downloadStatus,
|
||||||
required this.taskProgress,
|
required this.taskProgress,
|
||||||
required this.showProgress,
|
required this.showProgress,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:immich_mobile/models/folder/root_folder.model.dart';
|
|||||||
class RecursiveFolder extends RootFolder {
|
class RecursiveFolder extends RootFolder {
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
RecursiveFolder({
|
const RecursiveFolder({
|
||||||
required this.name,
|
required this.name,
|
||||||
required super.path,
|
required super.path,
|
||||||
required super.subfolders,
|
required super.subfolders,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class RootFolder {
|
|||||||
final List<RecursiveFolder> subfolders;
|
final List<RecursiveFolder> subfolders;
|
||||||
final String path;
|
final String path;
|
||||||
|
|
||||||
RootFolder({
|
const RootFolder({
|
||||||
required this.subfolders,
|
required this.subfolders,
|
||||||
required this.path,
|
required this.path,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ class MapAssetsInBoundsUpdated extends MapEvent {
|
|||||||
const MapAssetsInBoundsUpdated(this.assetRemoteIds);
|
const MapAssetsInBoundsUpdated(this.assetRemoteIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapCloseBottomSheet extends MapEvent {}
|
class MapCloseBottomSheet extends MapEvent {
|
||||||
|
const MapCloseBottomSheet();
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:openapi/api.dart';
|
|||||||
class MapMarker {
|
class MapMarker {
|
||||||
final LatLng latLng;
|
final LatLng latLng;
|
||||||
final String assetRemoteId;
|
final String assetRemoteId;
|
||||||
MapMarker({
|
const MapMarker({
|
||||||
required this.latLng,
|
required this.latLng,
|
||||||
required this.assetRemoteId,
|
required this.assetRemoteId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class MapState {
|
|||||||
final AsyncValue<String> lightStyleFetched;
|
final AsyncValue<String> lightStyleFetched;
|
||||||
final AsyncValue<String> darkStyleFetched;
|
final AsyncValue<String> darkStyleFetched;
|
||||||
|
|
||||||
MapState({
|
const MapState({
|
||||||
this.themeMode = ThemeMode.system,
|
this.themeMode = ThemeMode.system,
|
||||||
this.showFavoriteOnly = false,
|
this.showFavoriteOnly = false,
|
||||||
this.includeArchived = false,
|
this.includeArchived = false,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
|||||||
class Memory {
|
class Memory {
|
||||||
final String title;
|
final String title;
|
||||||
final List<Asset> assets;
|
final List<Asset> assets;
|
||||||
Memory({
|
const Memory({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.assets,
|
required this.assets,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class SearchCuratedContent {
|
|||||||
/// The id to lookup the asset from the server
|
/// The id to lookup the asset from the server
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
SearchCuratedContent({
|
const SearchCuratedContent({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.id,
|
required this.id,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class SearchResult {
|
|||||||
final List<Asset> assets;
|
final List<Asset> assets;
|
||||||
final int? nextPage;
|
final int? nextPage;
|
||||||
|
|
||||||
SearchResult({
|
const SearchResult({
|
||||||
required this.assets,
|
required this.assets,
|
||||||
this.nextPage,
|
this.nextPage,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class SearchResultPageState {
|
|||||||
final bool isSmart;
|
final bool isSmart;
|
||||||
final List<Asset> searchResult;
|
final List<Asset> searchResult;
|
||||||
|
|
||||||
SearchResultPageState({
|
const SearchResultPageState({
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.isSuccess,
|
required this.isSuccess,
|
||||||
required this.isError,
|
required this.isError,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ServerInfo {
|
|||||||
final bool isNewReleaseAvailable;
|
final bool isNewReleaseAvailable;
|
||||||
final String versionMismatchErrorMessage;
|
final String versionMismatchErrorMessage;
|
||||||
|
|
||||||
ServerInfo({
|
const ServerInfo({
|
||||||
required this.serverVersion,
|
required this.serverVersion,
|
||||||
required this.latestVersion,
|
required this.latestVersion,
|
||||||
required this.serverFeatures,
|
required this.serverFeatures,
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ class AlbumsPage extends HookConsumerWidget {
|
|||||||
color: context.colorScheme.onSurface.withAlpha(0),
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
width: 0,
|
width: 0,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||||
@@ -301,7 +303,9 @@ class QuickFilterButton extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
shape: WidgetStateProperty.all(
|
shape: WidgetStateProperty.all(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: context.colorScheme.onSurface.withAlpha(25),
|
color: context.colorScheme.onSurface.withAlpha(25),
|
||||||
width: 1,
|
width: 1,
|
||||||
@@ -334,8 +338,10 @@ class SortButton extends ConsumerWidget {
|
|||||||
style: MenuStyle(
|
style: MenuStyle(
|
||||||
elevation: const WidgetStatePropertyAll(1),
|
elevation: const WidgetStatePropertyAll(1),
|
||||||
shape: WidgetStateProperty.all(
|
shape: WidgetStateProperty.all(
|
||||||
RoundedRectangleBorder(
|
const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const WidgetStatePropertyAll(
|
padding: const WidgetStatePropertyAll(
|
||||||
@@ -384,8 +390,10 @@ class SortButton extends ConsumerWidget {
|
|||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
shape: WidgetStateProperty.all(
|
shape: WidgetStateProperty.all(
|
||||||
RoundedRectangleBorder(
|
const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -246,8 +246,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(10),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: context.colorScheme.outlineVariant,
|
color: context.colorScheme.outlineVariant,
|
||||||
width: 1,
|
width: 1,
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
|||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15), // if you need this
|
borderRadius: BorderRadius.all(
|
||||||
side: const BorderSide(
|
Radius.circular(15), // if you need this
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
color: Colors.black12,
|
color: Colors.black12,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surfaceContainerHigh,
|
color: context.colorScheme.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(15.0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@@ -99,7 +101,9 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surfaceContainerHigh,
|
color: context.colorScheme.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(15.0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
|||||||
@@ -120,8 +120,10 @@ class CreateAlbumPage extends HookConsumerWidget {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(10),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ class DownloadTaskTile extends StatelessWidget {
|
|||||||
width: context.width - 32,
|
width: context.width - 32,
|
||||||
child: Card(
|
child: Card(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(16),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
minVerticalPadding: 18,
|
minVerticalPadding: 18,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
||||||
@@ -11,7 +12,6 @@ import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
|||||||
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
|
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
|
|
||||||
enum SettingSection {
|
enum SettingSection {
|
||||||
advanced(
|
advanced(
|
||||||
@@ -85,12 +85,13 @@ class SettingsPage extends StatelessWidget {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: const Text('settings').tr(),
|
title: const Text('settings').tr(),
|
||||||
),
|
),
|
||||||
body: context.isMobile ? _MobileLayout() : _TabletLayout(),
|
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MobileLayout extends StatelessWidget {
|
class _MobileLayout extends StatelessWidget {
|
||||||
|
const _MobileLayout();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView(
|
return ListView(
|
||||||
@@ -147,6 +148,7 @@ class _MobileLayout extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TabletLayout extends HookWidget {
|
class _TabletLayout extends HookWidget {
|
||||||
|
const _TabletLayout();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final selectedSection =
|
final selectedSection =
|
||||||
|
|||||||
183
mobile/lib/pages/common/tab_shell.page.dart
Normal file
183
mobile/lib/pages/common/tab_shell.page.dart
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class TabShellPage extends ConsumerWidget {
|
||||||
|
const TabShellPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
|
||||||
|
Widget buildIcon({required Widget icon, required bool isProcessing}) {
|
||||||
|
if (!isProcessing) return icon;
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
Positioned(
|
||||||
|
right: -18,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onNavigationSelected(TabsRouter router, int index) {
|
||||||
|
// On Photos page menu tapped
|
||||||
|
if (router.activeIndex == 0 && index == 0) {
|
||||||
|
scrollToTopNotifierProvider.scrollToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Search page tapped
|
||||||
|
if (router.activeIndex == 1 && index == 1) {
|
||||||
|
ref.read(searchInputFocusProvider).requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
router.setActiveIndex(index);
|
||||||
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
final navigationDestinations = [
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'photos'.tr(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.photo_library_outlined,
|
||||||
|
),
|
||||||
|
selectedIcon: buildIcon(
|
||||||
|
isProcessing: false,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.photo_library,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'search'.tr(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.search_rounded,
|
||||||
|
),
|
||||||
|
selectedIcon: Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'albums'.tr(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.photo_album_outlined,
|
||||||
|
),
|
||||||
|
selectedIcon: buildIcon(
|
||||||
|
isProcessing: false,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.photo_album_rounded,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
label: 'library'.tr(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.space_dashboard_outlined,
|
||||||
|
),
|
||||||
|
selectedIcon: buildIcon(
|
||||||
|
isProcessing: false,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.space_dashboard_rounded,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Widget bottomNavigationBar(TabsRouter tabsRouter) {
|
||||||
|
return NavigationBar(
|
||||||
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
|
onDestinationSelected: (index) =>
|
||||||
|
onNavigationSelected(tabsRouter, index),
|
||||||
|
destinations: navigationDestinations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget navigationRail(TabsRouter tabsRouter) {
|
||||||
|
return NavigationRail(
|
||||||
|
destinations: navigationDestinations
|
||||||
|
.map(
|
||||||
|
(e) => NavigationRailDestination(
|
||||||
|
icon: e.icon,
|
||||||
|
label: Text(e.label),
|
||||||
|
selectedIcon: e.selectedIcon,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onDestinationSelected: (index) =>
|
||||||
|
onNavigationSelected(tabsRouter, index),
|
||||||
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
|
labelType: NavigationRailLabelType.all,
|
||||||
|
groupAlignment: 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final multiselectEnabled = ref.watch(multiselectProvider);
|
||||||
|
return AutoTabsRouter(
|
||||||
|
routes: [
|
||||||
|
const MainTimelineRoute(),
|
||||||
|
SearchRoute(),
|
||||||
|
const AlbumsRoute(),
|
||||||
|
const LibraryRoute(),
|
||||||
|
],
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
transitionBuilder: (context, child, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
builder: (context, child) {
|
||||||
|
final tabsRouter = AutoTabsRouter.of(context);
|
||||||
|
final heroedChild = HeroControllerScope(
|
||||||
|
controller: HeroController(),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
return PopScope(
|
||||||
|
canPop: tabsRouter.activeIndex == 0,
|
||||||
|
onPopInvokedWithResult: (didPop, _) =>
|
||||||
|
!didPop ? tabsRouter.setActiveIndex(0) : null,
|
||||||
|
child: Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
body: isScreenLandscape
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
navigationRail(tabsRouter),
|
||||||
|
const VerticalDivider(),
|
||||||
|
Expanded(child: heroedChild),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: heroedChild,
|
||||||
|
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
||||||
|
? null
|
||||||
|
: bottomNavigationBar(tabsRouter),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,7 +124,9 @@ class EditImagePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(7),
|
||||||
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.2),
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
@@ -135,7 +137,9 @@ class EditImagePage extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(7),
|
||||||
|
),
|
||||||
child: Image(
|
child: Image(
|
||||||
image: image.image,
|
image: image.image,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
@@ -149,7 +153,9 @@ class EditImagePage extends ConsumerWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10),
|
margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.scaffoldBackgroundColor,
|
color: context.scaffoldBackgroundColor,
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(30),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
|||||||
@@ -162,13 +162,17 @@ class _FilterButton extends StatelessWidget {
|
|||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(10),
|
||||||
|
),
|
||||||
border: isSelected
|
border: isSelected
|
||||||
? Border.all(color: context.primaryColor, width: 3)
|
? Border.all(color: context.primaryColor, width: 3)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(10),
|
||||||
|
),
|
||||||
child: ColorFiltered(
|
child: ColorFiltered(
|
||||||
colorFilter: filter,
|
colorFilter: filter,
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ class QuickAccessButtons extends ConsumerWidget {
|
|||||||
color: context.colorScheme.onSurface.withAlpha(10),
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
context.colorScheme.primary.withAlpha(10),
|
context.colorScheme.primary.withAlpha(10),
|
||||||
@@ -240,7 +242,9 @@ class PeopleCollectionCard extends ConsumerWidget {
|
|||||||
height: size,
|
height: size,
|
||||||
width: size,
|
width: size,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
context.colorScheme.primary.withAlpha(30),
|
context.colorScheme.primary.withAlpha(30),
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ class PartnerDetailPage extends HookConsumerWidget {
|
|||||||
color: context.colorScheme.onSurface.withAlpha(10),
|
color: context.colorScheme.onSurface.withAlpha(10),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
context.colorScheme.primary.withAlpha(10),
|
context.colorScheme.primary.withAlpha(10),
|
||||||
|
|||||||
@@ -143,7 +143,9 @@ class PlaceTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class MapPage extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
// If no asset was previously selected and no new asset is available, close the bottom sheet
|
// If no asset was previously selected and no new asset is available, close the bottom sheet
|
||||||
if (selectedMarker.value == null) {
|
if (selectedMarker.value == null) {
|
||||||
bottomSheetStreamController.add(MapCloseBottomSheet());
|
bottomSheetStreamController.add(const MapCloseBottomSheet());
|
||||||
}
|
}
|
||||||
selectedMarker.value = null;
|
selectedMarker.value = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -511,16 +511,11 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
search();
|
search();
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData getSearchPrefixIcon() {
|
IconData getSearchPrefixIcon() => switch (textSearchType.value) {
|
||||||
switch (textSearchType.value) {
|
TextSearchType.context => Icons.image_search_rounded,
|
||||||
case TextSearchType.context:
|
TextSearchType.filename => Icons.abc_rounded,
|
||||||
return Icons.image_search_rounded;
|
TextSearchType.description => Icons.text_snippet_outlined,
|
||||||
case TextSearchType.filename:
|
};
|
||||||
return Icons.abc_rounded;
|
|
||||||
case TextSearchType.description:
|
|
||||||
return Icons.text_snippet_outlined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
@@ -533,8 +528,10 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
style: MenuStyle(
|
style: MenuStyle(
|
||||||
elevation: const WidgetStatePropertyAll(1),
|
elevation: const WidgetStatePropertyAll(1),
|
||||||
shape: WidgetStateProperty.all(
|
shape: WidgetStateProperty.all(
|
||||||
RoundedRectangleBorder(
|
const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const WidgetStatePropertyAll(
|
padding: const WidgetStatePropertyAll(
|
||||||
@@ -631,7 +628,9 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
color: context.colorScheme.onSurface.withAlpha(0),
|
color: context.colorScheme.onSurface.withAlpha(0),
|
||||||
width: 0,
|
width: 0,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(24),
|
||||||
|
),
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
context.colorScheme.primary.withValues(alpha: 0.075),
|
context.colorScheme.primary.withValues(alpha: 0.075),
|
||||||
@@ -823,7 +822,9 @@ class QuickLinkList extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20),
|
||||||
|
),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: context.colorScheme.outline.withAlpha(10),
|
color: context.colorScheme.outline.withAlpha(10),
|
||||||
width: 1,
|
width: 1,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ final _features = [
|
|||||||
_Feature(
|
_Feature(
|
||||||
name: 'Main Timeline',
|
name: 'Main Timeline',
|
||||||
icon: Icons.timeline_rounded,
|
icon: Icons.timeline_rounded,
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const MainTimelineRoute()),
|
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
|||||||
final double width;
|
final double width;
|
||||||
final CacheManager? cacheManager;
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
RemoteThumbProvider({
|
const RemoteThumbProvider({
|
||||||
required this.assetId,
|
required this.assetId,
|
||||||
this.height = kTimelineFixedTileExtent,
|
this.height = kTimelineFixedTileExtent,
|
||||||
this.width = kTimelineFixedTileExtent,
|
this.width = kTimelineFixedTileExtent,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:thumbhash/thumbhash.dart';
|
|||||||
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
|
class ThumbHashProvider extends ImageProvider<ThumbHashProvider> {
|
||||||
final String thumbHash;
|
final String thumbHash;
|
||||||
|
|
||||||
ThumbHashProvider({
|
const ThumbHashProvider({
|
||||||
required this.thumbHash,
|
required this.thumbHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class FixedSegment extends Segment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleOnTap(WidgetRef ref, BaseAsset asset) {
|
void _handleOnTap(WidgetRef ref, BaseAsset asset) {
|
||||||
if (!ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
|
final multiSelectState = ref.read(multiSelectProvider);
|
||||||
|
if (!multiSelectState.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,8 @@ class FixedSegment extends Segment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
|
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
|
||||||
if (ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
|
final multiSelectState = ref.read(multiSelectProvider);
|
||||||
|
if (multiSelectState.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,55 +100,63 @@ class FixedSegment extends Segment {
|
|||||||
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRow(int assetIndex, int count) => Consumer(
|
Widget _buildRow(int assetIndex, int count) => RepaintBoundary(
|
||||||
builder: (ctx, ref, _) {
|
child: Consumer(
|
||||||
final isScrubbing =
|
builder: (ctx, ref, _) {
|
||||||
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
final isScrubbing =
|
||||||
final timelineService = ref.read(timelineServiceProvider);
|
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||||
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
|
|
||||||
// Timeline is being scrubbed, show placeholders
|
// Create stable callback references to prevent unnecessary rebuilds
|
||||||
if (isScrubbing) {
|
onTap(BaseAsset asset) => _handleOnTap(ref, asset);
|
||||||
return SegmentBuilder.buildPlaceholder(
|
onLongPress(BaseAsset asset) => _handleOnLongPress(ref, asset);
|
||||||
ctx,
|
|
||||||
count,
|
|
||||||
size: Size.square(tileHeight),
|
|
||||||
spacing: spacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bucket is already loaded, show the assets
|
// Timeline is being scrubbed, show placeholders
|
||||||
if (timelineService.hasRange(assetIndex, count)) {
|
if (isScrubbing) {
|
||||||
final assets = timelineService.getAssets(assetIndex, count);
|
return SegmentBuilder.buildPlaceholder(
|
||||||
return _buildAssetRow(
|
ctx,
|
||||||
ctx,
|
count,
|
||||||
assets,
|
size: Size.square(tileHeight),
|
||||||
onTap: (asset) => _handleOnTap(ref, asset),
|
spacing: spacing,
|
||||||
onLongPress: (asset) => _handleOnLongPress(ref, asset),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bucket is not loaded, show placeholders and load the bucket
|
|
||||||
return FutureBuilder(
|
|
||||||
future: timelineService.loadAssets(assetIndex, count),
|
|
||||||
builder: (ctxx, snap) {
|
|
||||||
if (snap.connectionState != ConnectionState.done) {
|
|
||||||
return SegmentBuilder.buildPlaceholder(
|
|
||||||
ctx,
|
|
||||||
count,
|
|
||||||
size: Size.square(tileHeight),
|
|
||||||
spacing: spacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _buildAssetRow(
|
|
||||||
ctxx,
|
|
||||||
snap.requireData,
|
|
||||||
onTap: (asset) => _handleOnTap(ref, asset),
|
|
||||||
onLongPress: (asset) => _handleOnLongPress(ref, asset),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
},
|
// Bucket is already loaded, show the assets
|
||||||
|
if (timelineService.hasRange(assetIndex, count)) {
|
||||||
|
final assets = timelineService.getAssets(assetIndex, count);
|
||||||
|
return _buildAssetRow(
|
||||||
|
ctx,
|
||||||
|
assets,
|
||||||
|
baseAssetIndex: assetIndex,
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket is not loaded, show placeholders and load the bucket
|
||||||
|
return FutureBuilder(
|
||||||
|
future: timelineService.loadAssets(assetIndex, count),
|
||||||
|
builder: (ctxx, snap) {
|
||||||
|
if (snap.connectionState != ConnectionState.done) {
|
||||||
|
return SegmentBuilder.buildPlaceholder(
|
||||||
|
ctx,
|
||||||
|
count,
|
||||||
|
size: Size.square(tileHeight),
|
||||||
|
spacing: spacing,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildAssetRow(
|
||||||
|
ctxx,
|
||||||
|
snap.requireData,
|
||||||
|
baseAssetIndex: assetIndex,
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAssetRow(
|
Widget _buildAssetRow(
|
||||||
@@ -154,6 +164,7 @@ class FixedSegment extends Segment {
|
|||||||
List<BaseAsset> assets, {
|
List<BaseAsset> assets, {
|
||||||
required void Function(BaseAsset) onTap,
|
required void Function(BaseAsset) onTap,
|
||||||
required void Function(BaseAsset) onLongPress,
|
required void Function(BaseAsset) onLongPress,
|
||||||
|
required int baseAssetIndex,
|
||||||
}) =>
|
}) =>
|
||||||
FixedTimelineRow(
|
FixedTimelineRow(
|
||||||
dimension: tileHeight,
|
dimension: tileHeight,
|
||||||
@@ -161,13 +172,59 @@ class FixedSegment extends Segment {
|
|||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
assets.length,
|
assets.length,
|
||||||
(i) => RepaintBoundary(
|
(i) => _AssetTileWidget(
|
||||||
child: GestureDetector(
|
key: ValueKey(_generateUniqueKey(assets[i], baseAssetIndex + i)),
|
||||||
onTap: () => onTap(assets[i]),
|
asset: assets[i],
|
||||||
onLongPress: () => onLongPress(assets[i]),
|
onTap: onTap,
|
||||||
child: ThumbnailTile(assets[i]),
|
onLongPress: onLongPress,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Generates a unique key for an asset that handles different asset types
|
||||||
|
/// and prevents duplicate keys even when assets have the same name/timestamp
|
||||||
|
String _generateUniqueKey(BaseAsset asset, int assetIndex) {
|
||||||
|
// Try to get the most unique identifier based on asset type
|
||||||
|
if (asset is Asset) {
|
||||||
|
// For remote/merged assets, use the remote ID which is globally unique
|
||||||
|
return 'asset_${asset.id}';
|
||||||
|
} else if (asset is LocalAsset) {
|
||||||
|
// For local assets, use the local ID which should be unique per device
|
||||||
|
return 'local_${asset.id}';
|
||||||
|
} else {
|
||||||
|
// Fallback for any other BaseAsset implementation
|
||||||
|
// Use checksum if available for additional uniqueness
|
||||||
|
final checksum = asset.checksum;
|
||||||
|
if (checksum != null && checksum.isNotEmpty) {
|
||||||
|
return 'checksum_${checksum.hashCode}';
|
||||||
|
} else {
|
||||||
|
// Last resort: use global asset index + object hash for uniqueness
|
||||||
|
return 'fallback_${assetIndex}_${asset.hashCode}_${asset.createdAt.microsecondsSinceEpoch}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetTileWidget extends StatelessWidget {
|
||||||
|
final BaseAsset asset;
|
||||||
|
final void Function(BaseAsset) onTap;
|
||||||
|
final void Function(BaseAsset) onLongPress;
|
||||||
|
|
||||||
|
const _AssetTileWidget({
|
||||||
|
super.key,
|
||||||
|
required this.asset,
|
||||||
|
required this.onTap,
|
||||||
|
required this.onLongPress,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RepaintBoundary(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onTap(asset),
|
||||||
|
onLongPress: () => onLongPress(asset),
|
||||||
|
child: ThumbnailTile(asset),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
@@ -80,12 +81,15 @@ class TimelineHeader extends ConsumerWidget {
|
|||||||
if (header != HeaderType.monthAndDay)
|
if (header != HeaderType.monthAndDay)
|
||||||
_BulkSelectIconButton(
|
_BulkSelectIconButton(
|
||||||
isAllSelected: isAllSelected,
|
isAllSelected: isAllSelected,
|
||||||
onPressed: () => ref
|
onPressed: () {
|
||||||
.read(multiSelectProvider.notifier)
|
ref
|
||||||
.toggleBucketSelection(
|
.read(multiSelectProvider.notifier)
|
||||||
assetOffset,
|
.toggleBucketSelection(
|
||||||
bucket.assetCount,
|
assetOffset,
|
||||||
),
|
bucket.assetCount,
|
||||||
|
);
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -101,9 +105,15 @@ class TimelineHeader extends ConsumerWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
_BulkSelectIconButton(
|
_BulkSelectIconButton(
|
||||||
isAllSelected: isAllSelected,
|
isAllSelected: isAllSelected,
|
||||||
onPressed: () => ref
|
onPressed: () {
|
||||||
.read(multiSelectProvider.notifier)
|
ref
|
||||||
.toggleBucketSelection(assetOffset, bucket.assetCount),
|
.read(multiSelectProvider.notifier)
|
||||||
|
.toggleBucketSelection(
|
||||||
|
assetOffset,
|
||||||
|
bucket.assetCount,
|
||||||
|
);
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -44,12 +44,16 @@ List<_Segment> _buildSegments({
|
|||||||
required List<Segment> layoutSegments,
|
required List<Segment> layoutSegments,
|
||||||
required double timelineHeight,
|
required double timelineHeight,
|
||||||
}) {
|
}) {
|
||||||
|
const double offsetThreshold = 20.0;
|
||||||
|
|
||||||
final segments = <_Segment>[];
|
final segments = <_Segment>[];
|
||||||
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
|
if (layoutSegments.isEmpty || layoutSegments.first.bucket is! TimeBucket) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
final formatter = DateFormat.yMMM();
|
final formatter = DateFormat.yMMM();
|
||||||
|
DateTime? lastDate;
|
||||||
|
double lastOffset = -offsetThreshold;
|
||||||
for (final layoutSegment in layoutSegments) {
|
for (final layoutSegment in layoutSegments) {
|
||||||
final scrollPercentage =
|
final scrollPercentage =
|
||||||
layoutSegment.startOffset / layoutSegments.last.endOffset;
|
layoutSegment.startOffset / layoutSegments.last.endOffset;
|
||||||
@@ -58,13 +62,21 @@ List<_Segment> _buildSegments({
|
|||||||
final date = (layoutSegment.bucket as TimeBucket).date;
|
final date = (layoutSegment.bucket as TimeBucket).date;
|
||||||
final label = formatter.format(date);
|
final label = formatter.format(date);
|
||||||
|
|
||||||
|
final showSegment = lastOffset + offsetThreshold <= startOffset &&
|
||||||
|
(lastDate == null || date.year != lastDate.year);
|
||||||
|
|
||||||
segments.add(
|
segments.add(
|
||||||
_Segment(
|
_Segment(
|
||||||
date: date,
|
date: date,
|
||||||
startOffset: startOffset,
|
startOffset: startOffset,
|
||||||
scrollLabel: label,
|
scrollLabel: label,
|
||||||
|
showSegment: showSegment,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
lastDate = date;
|
||||||
|
if (showSegment) {
|
||||||
|
lastOffset = startOffset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
@@ -85,12 +97,15 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
|||||||
double get _scrubberHeight =>
|
double get _scrubberHeight =>
|
||||||
widget.timelineHeight - widget.topPadding - widget.bottomPadding;
|
widget.timelineHeight - widget.topPadding - widget.bottomPadding;
|
||||||
|
|
||||||
late final ScrollController _scrollController;
|
late ScrollController _scrollController;
|
||||||
|
|
||||||
double get _currentOffset =>
|
double get _currentOffset {
|
||||||
_scrollController.offset *
|
if (_scrollController.hasClients != true) return 0.0;
|
||||||
_scrubberHeight /
|
|
||||||
_scrollController.position.maxScrollExtent;
|
return _scrollController.offset *
|
||||||
|
_scrubberHeight /
|
||||||
|
_scrollController.position.maxScrollExtent;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -194,28 +209,102 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
|||||||
_thumbAnimationController.forward();
|
_thumbAnimationController.forward();
|
||||||
}
|
}
|
||||||
|
|
||||||
final newOffset =
|
final dragPosition = _calculateDragPosition(details);
|
||||||
details.globalPosition.dy - widget.topPadding - widget.bottomPadding;
|
final nearestMonthSegment = _findNearestMonthSegment(dragPosition);
|
||||||
|
|
||||||
|
if (nearestMonthSegment != null) {
|
||||||
|
_snapToSegment(nearestMonthSegment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the drag position relative to the scrubber area
|
||||||
|
///
|
||||||
|
/// This method converts the global drag coordinates from the gesture detector
|
||||||
|
/// into a position relative to the scrubber's active area (excluding padding).
|
||||||
|
///
|
||||||
|
/// The scrubber has padding at the top and bottom, so we need to:
|
||||||
|
/// 1. Calculate the actual draggable area (timelineHeight - topPadding - bottomPadding)
|
||||||
|
/// 2. Convert the global Y position to a position within this draggable area
|
||||||
|
/// 3. Clamp the result to ensure it stays within bounds (0 to dragAreaHeight)
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// - If timelineHeight = 800, topPadding = 50, bottomPadding = 50
|
||||||
|
/// - Then dragAreaHeight = 700 (the actual scrubber area)
|
||||||
|
/// - If user drags to global Y position that's 100 pixels from the top
|
||||||
|
/// - The relative position would be 100 - 50 = 50 (50 pixels into the scrubber area)
|
||||||
|
double _calculateDragPosition(DragUpdateDetails details) {
|
||||||
|
final dragAreaTop = widget.topPadding;
|
||||||
|
final dragAreaBottom = widget.timelineHeight - widget.bottomPadding;
|
||||||
|
final dragAreaHeight = dragAreaBottom - dragAreaTop;
|
||||||
|
|
||||||
|
final relativePosition = details.globalPosition.dy - dragAreaTop;
|
||||||
|
|
||||||
|
// Make sure the position stays within the scrubber's bounds
|
||||||
|
return relativePosition.clamp(0.0, dragAreaHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the segment closest to the given position
|
||||||
|
_Segment? _findNearestMonthSegment(double position) {
|
||||||
|
_Segment? nearestSegment;
|
||||||
|
double minDistance = double.infinity;
|
||||||
|
|
||||||
|
for (final segment in _segments) {
|
||||||
|
final distance = (segment.startOffset - position).abs();
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
nearestSegment = segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearestSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snap the scrubber thumb and scroll view to the given segment
|
||||||
|
void _snapToSegment(_Segment segment) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_thumbTopOffset = newOffset.clamp(0, _scrubberHeight);
|
_thumbTopOffset = segment.startOffset;
|
||||||
final scrollPercentage = _thumbTopOffset / _scrubberHeight;
|
|
||||||
final maxScrollExtent = _scrollController.position.maxScrollExtent;
|
final layoutSegmentIndex = _findLayoutSegmentIndex(segment);
|
||||||
_scrollController.jumpTo(maxScrollExtent * scrollPercentage);
|
|
||||||
|
if (layoutSegmentIndex >= 0) {
|
||||||
|
_scrollToLayoutSegment(layoutSegmentIndex);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _findLayoutSegmentIndex(_Segment segment) {
|
||||||
|
return widget.layoutSegments.indexWhere(
|
||||||
|
(layoutSegment) {
|
||||||
|
final bucket = layoutSegment.bucket as TimeBucket;
|
||||||
|
return bucket.date.year == segment.date.year &&
|
||||||
|
bucket.date.month == segment.date.month;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToLayoutSegment(int layoutSegmentIndex) {
|
||||||
|
final layoutSegment = widget.layoutSegments[layoutSegmentIndex];
|
||||||
|
final maxScrollExtent = _scrollController.position.maxScrollExtent;
|
||||||
|
final viewportHeight = _scrollController.position.viewportDimension;
|
||||||
|
|
||||||
|
final targetScrollOffset = layoutSegment.startOffset;
|
||||||
|
final centeredOffset = targetScrollOffset - (viewportHeight / 4) + 100;
|
||||||
|
|
||||||
|
_scrollController.jumpTo(centeredOffset.clamp(0.0, maxScrollExtent));
|
||||||
|
}
|
||||||
|
|
||||||
void _onDragEnd(WidgetRef ref) {
|
void _onDragEnd(WidgetRef ref) {
|
||||||
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||||
_labelAnimationController.reverse();
|
_labelAnimationController.reverse();
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
|
|
||||||
_resetThumbTimer();
|
_resetThumbTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext ctx) {
|
Widget build(BuildContext ctx) {
|
||||||
Text? label;
|
Text? label;
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients == true) {
|
||||||
// Cache to avoid multiple calls to [_currentOffset]
|
// Cache to avoid multiple calls to [_currentOffset]
|
||||||
final scrollOffset = _currentOffset;
|
final scrollOffset = _currentOffset;
|
||||||
final labelText = _segments
|
final labelText = _segments
|
||||||
@@ -240,20 +329,31 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
RepaintBoundary(child: widget.child),
|
RepaintBoundary(child: widget.child),
|
||||||
|
// Scroll Segments - wrapped in RepaintBoundary for better performance
|
||||||
|
RepaintBoundary(
|
||||||
|
child: _SegmentsLayer(
|
||||||
|
key: ValueKey('segments_${_isDragging}_${_segments.length}'),
|
||||||
|
segments: _segments,
|
||||||
|
topPadding: widget.topPadding,
|
||||||
|
isDragging: _isDragging,
|
||||||
|
),
|
||||||
|
),
|
||||||
PositionedDirectional(
|
PositionedDirectional(
|
||||||
top: _thumbTopOffset + widget.topPadding,
|
top: _thumbTopOffset + widget.topPadding,
|
||||||
end: 0,
|
end: 0,
|
||||||
child: Consumer(
|
child: RepaintBoundary(
|
||||||
builder: (_, ref, child) => GestureDetector(
|
child: Consumer(
|
||||||
onVerticalDragStart: (_) => _onDragStart(ref),
|
builder: (_, ref, child) => GestureDetector(
|
||||||
onVerticalDragUpdate: _onDragUpdate,
|
onVerticalDragStart: (_) => _onDragStart(ref),
|
||||||
onVerticalDragEnd: (_) => _onDragEnd(ref),
|
onVerticalDragUpdate: _onDragUpdate,
|
||||||
child: child,
|
onVerticalDragEnd: (_) => _onDragEnd(ref),
|
||||||
),
|
child: child,
|
||||||
child: _Scrubber(
|
),
|
||||||
thumbAnimation: _thumbAnimation,
|
child: _Scrubber(
|
||||||
labelAnimation: _labelAnimation,
|
thumbAnimation: _thumbAnimation,
|
||||||
label: label,
|
labelAnimation: _labelAnimation,
|
||||||
|
label: label,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -263,6 +363,72 @@ class ScrubberState extends State<Scrubber> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SegmentsLayer extends StatelessWidget {
|
||||||
|
final List<_Segment> segments;
|
||||||
|
final double topPadding;
|
||||||
|
final bool isDragging;
|
||||||
|
|
||||||
|
const _SegmentsLayer({
|
||||||
|
super.key,
|
||||||
|
required this.segments,
|
||||||
|
required this.topPadding,
|
||||||
|
required this.isDragging,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Visibility(
|
||||||
|
visible: isDragging,
|
||||||
|
child: Stack(
|
||||||
|
children: segments
|
||||||
|
.where((segment) => segment.showSegment)
|
||||||
|
.map(
|
||||||
|
(segment) => PositionedDirectional(
|
||||||
|
key: ValueKey('segment_${segment.date.millisecondsSinceEpoch}'),
|
||||||
|
top: topPadding + segment.startOffset,
|
||||||
|
end: 100,
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: _SegmentWidget(segment),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SegmentWidget extends StatelessWidget {
|
||||||
|
final _Segment _segment;
|
||||||
|
|
||||||
|
const _SegmentWidget(this._segment);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(right: 12.0),
|
||||||
|
child: Material(
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 28),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
_segment.date.year.toString(),
|
||||||
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
|
fontFamily: "OverpassMono",
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ScrollLabel extends StatelessWidget {
|
class _ScrollLabel extends StatelessWidget {
|
||||||
final Text label;
|
final Text label;
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
@@ -429,22 +595,26 @@ class _Segment {
|
|||||||
final DateTime date;
|
final DateTime date;
|
||||||
final double startOffset;
|
final double startOffset;
|
||||||
final String scrollLabel;
|
final String scrollLabel;
|
||||||
|
final bool showSegment;
|
||||||
|
|
||||||
const _Segment({
|
const _Segment({
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.startOffset,
|
required this.startOffset,
|
||||||
required this.scrollLabel,
|
required this.scrollLabel,
|
||||||
|
this.showSegment = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
_Segment copyWith({
|
_Segment copyWith({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
double? startOffset,
|
double? startOffset,
|
||||||
String? scrollLabel,
|
String? scrollLabel,
|
||||||
|
bool? showSegment,
|
||||||
}) {
|
}) {
|
||||||
return _Segment(
|
return _Segment(
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
startOffset: startOffset ?? this.startOffset,
|
startOffset: startOffset ?? this.startOffset,
|
||||||
scrollLabel: scrollLabel ?? this.scrollLabel,
|
scrollLabel: scrollLabel ?? this.scrollLabel,
|
||||||
|
showSegment: showSegment ?? this.showSegment,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,12 @@ abstract class SegmentBuilder {
|
|||||||
this.groupBy = GroupAssetsBy.day,
|
this.groupBy = GroupAssetsBy.day,
|
||||||
});
|
});
|
||||||
|
|
||||||
static double headerExtent(HeaderType header) {
|
static double headerExtent(HeaderType header) => switch (header) {
|
||||||
switch (header) {
|
HeaderType.month => kTimelineHeaderExtent,
|
||||||
case HeaderType.month:
|
HeaderType.day => kTimelineHeaderExtent * 0.90,
|
||||||
return kTimelineHeaderExtent;
|
HeaderType.monthAndDay => kTimelineHeaderExtent * 1.6,
|
||||||
case HeaderType.day:
|
HeaderType.none => 0.0,
|
||||||
return kTimelineHeaderExtent * 0.90;
|
};
|
||||||
case HeaderType.monthAndDay:
|
|
||||||
return kTimelineHeaderExtent * 1.6;
|
|
||||||
case HeaderType.none:
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Widget buildPlaceholder(
|
static Widget buildPlaceholder(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
|
|
||||||
class Timeline extends StatelessWidget {
|
class Timeline extends StatelessWidget {
|
||||||
const Timeline({super.key});
|
const Timeline({super.key});
|
||||||
@@ -63,38 +65,68 @@ class _SliverTimelineState extends State<_SliverTimeline> {
|
|||||||
final asyncSegments = ref.watch(timelineSegmentProvider);
|
final asyncSegments = ref.watch(timelineSegmentProvider);
|
||||||
final maxHeight =
|
final maxHeight =
|
||||||
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
||||||
|
final isMultiSelectEnabled =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
return asyncSegments.widgetWhen(
|
return asyncSegments.widgetWhen(
|
||||||
onData: (segments) {
|
onData: (segments) {
|
||||||
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
||||||
|
final statusBarHeight = context.padding.top;
|
||||||
|
final totalAppBarHeight = statusBarHeight + kToolbarHeight;
|
||||||
|
const scrubberBottomPadding = 100.0;
|
||||||
|
|
||||||
return PrimaryScrollController(
|
return PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: Scrubber(
|
child: Stack(
|
||||||
layoutSegments: segments,
|
children: [
|
||||||
timelineHeight: maxHeight,
|
Scrubber(
|
||||||
topPadding: context.padding.top + 10,
|
layoutSegments: segments,
|
||||||
bottomPadding: context.padding.bottom + 10,
|
timelineHeight: maxHeight,
|
||||||
child: CustomScrollView(
|
topPadding: totalAppBarHeight + 10,
|
||||||
primary: true,
|
bottomPadding:
|
||||||
cacheExtent: maxHeight * 2,
|
context.padding.bottom + scrubberBottomPadding,
|
||||||
slivers: [
|
child: CustomScrollView(
|
||||||
_SliverSegmentedList(
|
primary: true,
|
||||||
segments: segments,
|
cacheExtent: maxHeight * 2,
|
||||||
delegate: SliverChildBuilderDelegate(
|
slivers: [
|
||||||
(ctx, index) {
|
SliverAnimatedOpacity(
|
||||||
if (index >= childCount) return null;
|
duration: Durations.medium1,
|
||||||
final segment = segments.findByIndex(index);
|
opacity: isMultiSelectEnabled ? 0 : 1,
|
||||||
return segment?.builder(ctx, index) ??
|
sliver: const ImmichSliverAppBar(
|
||||||
const SizedBox.shrink();
|
floating: true,
|
||||||
},
|
pinned: false,
|
||||||
childCount: childCount,
|
snap: false,
|
||||||
addAutomaticKeepAlives: false,
|
),
|
||||||
// We add repaint boundary around tiles, so skip the auto boundaries
|
),
|
||||||
addRepaintBoundaries: false,
|
_SliverSegmentedList(
|
||||||
),
|
segments: segments,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, index) {
|
||||||
|
if (index >= childCount) return null;
|
||||||
|
final segment = segments.findByIndex(index);
|
||||||
|
return segment?.builder(ctx, index) ??
|
||||||
|
const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
childCount: childCount,
|
||||||
|
addAutomaticKeepAlives: false,
|
||||||
|
// We add repaint boundary around tiles, so skip the auto boundaries
|
||||||
|
addRepaintBoundaries: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverPadding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: scrubberBottomPadding,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
if (isMultiSelectEnabled)
|
||||||
|
const Positioned(
|
||||||
|
top: 60,
|
||||||
|
left: 25,
|
||||||
|
child: _MultiSelectStatusButton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -363,3 +395,27 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
|||||||
childManager.didFinishLayout();
|
childManager.didFinishLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MultiSelectStatusButton extends ConsumerWidget {
|
||||||
|
const _MultiSelectStatusButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final selectCount =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length));
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: () => ref.read(multiSelectProvider.notifier).clearSelection(),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.close_rounded,
|
||||||
|
color: context.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
selectCount.toString(),
|
||||||
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
|
height: 2.5,
|
||||||
|
color: context.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
|
import 'package:immich_mobile/models/albums/album_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
|
||||||
|
|
||||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
AlbumViewerNotifier(this.ref)
|
AlbumViewerNotifier(this.ref)
|
||||||
: super(
|
: super(
|
||||||
AlbumViewerPageState(
|
const AlbumViewerPageState(
|
||||||
editTitleText: "",
|
editTitleText: "",
|
||||||
isEditAlbum: false,
|
isEditAlbum: false,
|
||||||
editDescriptionText: "",
|
editDescriptionText: "",
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
part 'app_settings.provider.g.dart';
|
part 'app_settings.provider.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
AppSettingsService appSettingsService(Ref _) => AppSettingsService();
|
AppSettingsService appSettingsService(Ref _) => const AppSettingsService();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'app_settings.provider.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$appSettingsServiceHash() =>
|
String _$appSettingsServiceHash() =>
|
||||||
r'2aa16d76a8df869c39486325efc1d08b2d2c284c';
|
r'89cece3a19e06612f5639ae290120e854a0c5a31';
|
||||||
|
|
||||||
/// See also [appSettingsService].
|
/// See also [appSettingsService].
|
||||||
@ProviderFor(appSettingsService)
|
@ProviderFor(appSettingsService)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
|
|||||||
this._shareService,
|
this._shareService,
|
||||||
this._albumService,
|
this._albumService,
|
||||||
) : super(
|
) : super(
|
||||||
DownloadState(
|
const DownloadState(
|
||||||
downloadStatus: TaskStatus.complete,
|
downloadStatus: TaskStatus.complete,
|
||||||
showProgress: false,
|
showProgress: false,
|
||||||
taskProgress: <String, DownloadInfo>{},
|
taskProgress: <String, DownloadInfo>{},
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
this._secureStorageService,
|
this._secureStorageService,
|
||||||
this._widgetService,
|
this._widgetService,
|
||||||
) : super(
|
) : super(
|
||||||
AuthState(
|
const AuthState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
@@ -89,7 +89,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _cleanUp() async {
|
Future<void> _cleanUp() async {
|
||||||
state = AuthState(
|
state = const AuthState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class IOSBackgroundSettings {
|
|||||||
final DateTime? timeOfLastFetch;
|
final DateTime? timeOfLastFetch;
|
||||||
final DateTime? timeOfLastProcessing;
|
final DateTime? timeOfLastProcessing;
|
||||||
|
|
||||||
IOSBackgroundSettings({
|
const IOSBackgroundSettings({
|
||||||
required this.appRefreshEnabled,
|
required this.appRefreshEnabled,
|
||||||
required this.numberOfBackgroundTasksQueued,
|
required this.numberOfBackgroundTasksQueued,
|
||||||
this.timeOfLastFetch,
|
this.timeOfLastFetch,
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ class ImageLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, the image failed to load from the cache stream
|
// If we get here, the image failed to load from the cache stream
|
||||||
throw ImageLoadingException('Could not load image from stream');
|
throw const ImageLoadingException('Could not load image from stream');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/// An exception for the [ImageLoader] and the Immich image providers
|
/// An exception for the [ImageLoader] and the Immich image providers
|
||||||
class ImageLoadingException implements Exception {
|
class ImageLoadingException implements Exception {
|
||||||
final String message;
|
final String message;
|
||||||
ImageLoadingException(this.message);
|
const ImageLoadingException(this.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ImmichRemoteImageProvider
|
|||||||
/// The image cache manager
|
/// The image cache manager
|
||||||
final CacheManager? cacheManager;
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
ImmichRemoteImageProvider({
|
const ImmichRemoteImageProvider({
|
||||||
required this.assetId,
|
required this.assetId,
|
||||||
this.cacheManager,
|
this.cacheManager,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ImmichRemoteThumbnailProvider
|
|||||||
/// The image cache manager
|
/// The image cache manager
|
||||||
final CacheManager? cacheManager;
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
ImmichRemoteThumbnailProvider({
|
const ImmichRemoteThumbnailProvider({
|
||||||
required this.assetId,
|
required this.assetId,
|
||||||
this.height,
|
this.height,
|
||||||
this.width,
|
this.width,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
|||||||
final SearchService _searchService;
|
final SearchService _searchService;
|
||||||
|
|
||||||
PaginatedSearchNotifier(this._searchService)
|
PaginatedSearchNotifier(this._searchService)
|
||||||
: super(SearchResult(assets: [], nextPage: 1));
|
: super(const SearchResult(assets: [], nextPage: 1));
|
||||||
|
|
||||||
Future<bool> search(SearchFilter filter) async {
|
Future<bool> search(SearchFilter filter) async {
|
||||||
if (state.nextPage == null) {
|
if (state.nextPage == null) {
|
||||||
@@ -39,7 +39,7 @@ class PaginatedSearchNotifier extends StateNotifier<SearchResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
state = SearchResult(assets: [], nextPage: 1);
|
state = const SearchResult(assets: [], nextPage: 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
|
||||||
import 'package:immich_mobile/services/server_info.service.dart';
|
|
||||||
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
import 'package:immich_mobile/models/server_info/server_config.model.dart';
|
||||||
|
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
||||||
|
import 'package:immich_mobile/models/server_info/server_info.model.dart';
|
||||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||||
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
||||||
ServerInfoNotifier(this._serverInfoService)
|
ServerInfoNotifier(this._serverInfoService)
|
||||||
: super(
|
: super(
|
||||||
ServerInfo(
|
const ServerInfo(
|
||||||
serverVersion: const ServerVersion(
|
serverVersion: ServerVersion(
|
||||||
major: 0,
|
major: 0,
|
||||||
minor: 0,
|
minor: 0,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
),
|
),
|
||||||
latestVersion: const ServerVersion(
|
latestVersion: ServerVersion(
|
||||||
major: 0,
|
major: 0,
|
||||||
minor: 0,
|
minor: 0,
|
||||||
patch: 0,
|
patch: 0,
|
||||||
),
|
),
|
||||||
serverFeatures: const ServerFeatures(
|
serverFeatures: ServerFeatures(
|
||||||
map: true,
|
map: true,
|
||||||
trash: true,
|
trash: true,
|
||||||
oauthEnabled: false,
|
oauthEnabled: false,
|
||||||
passwordLogin: true,
|
passwordLogin: true,
|
||||||
),
|
),
|
||||||
serverConfig: const ServerConfig(
|
serverConfig: ServerConfig(
|
||||||
trashDays: 30,
|
trashDays: 30,
|
||||||
oauthButtonText: '',
|
oauthButtonText: '',
|
||||||
externalDomain: '',
|
externalDomain: '',
|
||||||
@@ -38,7 +37,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
|||||||
'https://tiles.immich.cloud/v1/style/light.json',
|
'https://tiles.immich.cloud/v1/style/light.json',
|
||||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||||
),
|
),
|
||||||
serverDiskInfo: const ServerDiskInfo(
|
serverDiskInfo: ServerDiskInfo(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
diskSize: "0",
|
diskSize: "0",
|
||||||
diskUse: "0",
|
diskUse: "0",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ final multiSelectProvider =
|
|||||||
class MultiSelectState {
|
class MultiSelectState {
|
||||||
final Set<BaseAsset> selectedAssets;
|
final Set<BaseAsset> selectedAssets;
|
||||||
|
|
||||||
MultiSelectState({
|
const MultiSelectState({
|
||||||
required this.selectedAssets,
|
required this.selectedAssets,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
|
|||||||
MultiSelectState build() {
|
MultiSelectState build() {
|
||||||
_timelineService = ref.read(timelineServiceProvider);
|
_timelineService = ref.read(timelineServiceProvider);
|
||||||
|
|
||||||
return MultiSelectState(
|
return const MultiSelectState(
|
||||||
selectedAssets: {},
|
selectedAssets: {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,12 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearSelection() {
|
||||||
|
state = state.copyWith(
|
||||||
|
selectedAssets: {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Bucket bulk operations
|
/// Bucket bulk operations
|
||||||
void selectBucket(int offset, int bucketCount) async {
|
void selectBucket(int offset, int bucketCount) async {
|
||||||
final assets = await _timelineService.loadAssets(offset, bucketCount);
|
final assets = await _timelineService.loadAssets(offset, bucketCount);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class UploadProfileImageState {
|
|||||||
// enum
|
// enum
|
||||||
final UploadProfileStatus status;
|
final UploadProfileStatus status;
|
||||||
final String profileImagePath;
|
final String profileImagePath;
|
||||||
UploadProfileImageState({
|
const UploadProfileImageState({
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
});
|
});
|
||||||
@@ -74,7 +74,7 @@ class UploadProfileImageNotifier
|
|||||||
extends StateNotifier<UploadProfileImageState> {
|
extends StateNotifier<UploadProfileImageState> {
|
||||||
UploadProfileImageNotifier(this._userService)
|
UploadProfileImageNotifier(this._userService)
|
||||||
: super(
|
: super(
|
||||||
UploadProfileImageState(
|
const UploadProfileImageState(
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
status: UploadProfileStatus.idle,
|
status: UploadProfileStatus.idle,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class WebsocketState {
|
|||||||
final bool isConnected;
|
final bool isConnected;
|
||||||
final List<PendingChange> pendingChanges;
|
final List<PendingChange> pendingChanges;
|
||||||
|
|
||||||
WebsocketState({
|
const WebsocketState({
|
||||||
this.socket,
|
this.socket,
|
||||||
required this.isConnected,
|
required this.isConnected,
|
||||||
required this.pendingChanges,
|
required this.pendingChanges,
|
||||||
@@ -94,7 +94,11 @@ class WebsocketState {
|
|||||||
class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
WebsocketNotifier(this._ref)
|
WebsocketNotifier(this._ref)
|
||||||
: super(
|
: super(
|
||||||
WebsocketState(socket: null, isConnected: false, pendingChanges: []),
|
const WebsocketState(
|
||||||
|
socket: null,
|
||||||
|
isConnected: false,
|
||||||
|
pendingChanges: [],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final _log = Logger('WebsocketNotifier');
|
final _log = Logger('WebsocketNotifier');
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ final albumRepositoryProvider =
|
|||||||
Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
|
Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
class AlbumRepository extends DatabaseRepository {
|
class AlbumRepository extends DatabaseRepository {
|
||||||
AlbumRepository(super.db);
|
const AlbumRepository(super.db);
|
||||||
|
|
||||||
Future<int> count({bool? local}) {
|
Future<int> count({bool? local}) {
|
||||||
final baseQuery = db.albums.where();
|
final baseQuery = db.albums.where();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:immich_mobile/constants/errors.dart';
|
|||||||
abstract class ApiRepository {
|
abstract class ApiRepository {
|
||||||
Future<T> checkNull<T>(Future<T?> future) async {
|
Future<T> checkNull<T>(Future<T?> future) async {
|
||||||
final response = await future;
|
final response = await future;
|
||||||
if (response == null) throw NoResponseDtoError();
|
if (response == null) throw const NoResponseDtoError();
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ final assetRepositoryProvider =
|
|||||||
Provider((ref) => AssetRepository(ref.watch(dbProvider)));
|
Provider((ref) => AssetRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
class AssetRepository extends DatabaseRepository {
|
class AssetRepository extends DatabaseRepository {
|
||||||
AssetRepository(super.db);
|
const AssetRepository(super.db);
|
||||||
|
|
||||||
Future<List<Asset>> getByAlbum(
|
Future<List<Asset>> getByAlbum(
|
||||||
Album album, {
|
Album album, {
|
||||||
|
|||||||
@@ -56,18 +56,12 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mapVisibility(AssetVisibilityEnum visibility) {
|
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
||||||
switch (visibility) {
|
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
||||||
case AssetVisibilityEnum.timeline:
|
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
||||||
return AssetVisibility.timeline;
|
AssetVisibilityEnum.locked => AssetVisibility.locked,
|
||||||
case AssetVisibilityEnum.hidden:
|
AssetVisibilityEnum.archive => AssetVisibility.archive,
|
||||||
return AssetVisibility.hidden;
|
};
|
||||||
case AssetVisibilityEnum.locked:
|
|
||||||
return AssetVisibility.locked;
|
|
||||||
case AssetVisibilityEnum.archive:
|
|
||||||
return AssetVisibility.archive;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> getAssetMIMEType(String assetId) async {
|
Future<String?> getAssetMIMEType(String assetId) async {
|
||||||
final response = await checkNull(_api.getAssetInfo(assetId));
|
final response = await checkNull(_api.getAssetInfo(assetId));
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/utils/hash.dart';
|
import 'package:immich_mobile/utils/hash.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||||
|
|
||||||
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
|
final assetMediaRepositoryProvider =
|
||||||
|
Provider((ref) => const AssetMediaRepository());
|
||||||
|
|
||||||
class AssetMediaRepository {
|
class AssetMediaRepository {
|
||||||
|
const AssetMediaRepository();
|
||||||
Future<List<String>> deleteAll(List<String> ids) =>
|
Future<List<String>> deleteAll(List<String> ids) =>
|
||||||
PhotoManager.editor.deleteWithIds(ids);
|
PhotoManager.editor.deleteWithIds(ids);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final authRepositoryProvider = Provider<AuthRepository>(
|
|||||||
class AuthRepository extends DatabaseRepository {
|
class AuthRepository extends DatabaseRepository {
|
||||||
final Drift _drift;
|
final Drift _drift;
|
||||||
|
|
||||||
AuthRepository(super.db, this._drift);
|
const AuthRepository(super.db, this._drift);
|
||||||
|
|
||||||
Future<void> clearLocalData() {
|
Future<void> clearLocalData() {
|
||||||
return db.writeTxn(() {
|
return db.writeTxn(() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ final backupAlbumRepositoryProvider =
|
|||||||
Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider)));
|
Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
class BackupAlbumRepository extends DatabaseRepository {
|
class BackupAlbumRepository extends DatabaseRepository {
|
||||||
BackupAlbumRepository(super.db);
|
const BackupAlbumRepository(super.db);
|
||||||
|
|
||||||
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
||||||
final baseQuery = db.backupAlbums.where();
|
final baseQuery = db.backupAlbums.where();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ final biometricRepositoryProvider =
|
|||||||
class BiometricRepository {
|
class BiometricRepository {
|
||||||
final LocalAuthentication _localAuth;
|
final LocalAuthentication _localAuth;
|
||||||
|
|
||||||
BiometricRepository(this._localAuth);
|
const BiometricRepository(this._localAuth);
|
||||||
|
|
||||||
Future<BiometricStatus> getStatus() async {
|
Future<BiometricStatus> getStatus() async {
|
||||||
final bool canAuthenticateWithBiometrics =
|
final bool canAuthenticateWithBiometrics =
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Symbol _zoneTxn = #zoneTxn;
|
|||||||
|
|
||||||
abstract class DatabaseRepository implements IDatabaseRepository {
|
abstract class DatabaseRepository implements IDatabaseRepository {
|
||||||
final Isar db;
|
final Isar db;
|
||||||
DatabaseRepository(this.db);
|
const DatabaseRepository(this.db);
|
||||||
|
|
||||||
bool get inTxn => Zone.current[_zoneTxn] != null;
|
bool get inTxn => Zone.current[_zoneTxn] != null;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ final etagRepositoryProvider =
|
|||||||
Provider((ref) => ETagRepository(ref.watch(dbProvider)));
|
Provider((ref) => ETagRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
class ETagRepository extends DatabaseRepository {
|
class ETagRepository extends DatabaseRepository {
|
||||||
ETagRepository(super.db);
|
const ETagRepository(super.db);
|
||||||
|
|
||||||
Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
|
Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
|||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||||
|
|
||||||
final fileMediaRepositoryProvider = Provider((ref) => FileMediaRepository());
|
final fileMediaRepositoryProvider =
|
||||||
|
Provider((ref) => const FileMediaRepository());
|
||||||
|
|
||||||
class FileMediaRepository {
|
class FileMediaRepository {
|
||||||
|
const FileMediaRepository();
|
||||||
Future<Asset?> saveImage(
|
Future<Asset?> saveImage(
|
||||||
Uint8List data, {
|
Uint8List data, {
|
||||||
required String title,
|
required String title,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ final localFilesManagerRepositoryProvider = Provider(
|
|||||||
);
|
);
|
||||||
|
|
||||||
class LocalFilesManagerRepository {
|
class LocalFilesManagerRepository {
|
||||||
LocalFilesManagerRepository(this._service);
|
const LocalFilesManagerRepository(this._service);
|
||||||
|
|
||||||
final LocalFilesManagerService _service;
|
final LocalFilesManagerService _service;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ final networkRepositoryProvider = Provider((_) {
|
|||||||
class NetworkRepository {
|
class NetworkRepository {
|
||||||
final NetworkInfo _networkInfo;
|
final NetworkInfo _networkInfo;
|
||||||
|
|
||||||
NetworkRepository(this._networkInfo);
|
const NetworkRepository(this._networkInfo);
|
||||||
|
|
||||||
Future<String?> getWifiName() {
|
Future<String?> getWifiName() {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ final partnerRepositoryProvider = Provider(
|
|||||||
);
|
);
|
||||||
|
|
||||||
class PartnerRepository extends DatabaseRepository {
|
class PartnerRepository extends DatabaseRepository {
|
||||||
PartnerRepository(super.db);
|
const PartnerRepository(super.db);
|
||||||
|
|
||||||
Future<List<UserDto>> getSharedBy() async {
|
Future<List<UserDto>> getSharedBy() async {
|
||||||
return (await db.users
|
return (await db.users
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
final permissionRepositoryProvider = Provider((_) {
|
final permissionRepositoryProvider = Provider((_) {
|
||||||
return PermissionRepository();
|
return const PermissionRepository();
|
||||||
});
|
});
|
||||||
|
|
||||||
class PermissionRepository implements IPermissionRepository {
|
class PermissionRepository implements IPermissionRepository {
|
||||||
PermissionRepository();
|
const PermissionRepository();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> hasLocationWhenInUsePermission() {
|
Future<bool> hasLocationWhenInUsePermission() {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
final secureStorageRepositoryProvider =
|
final secureStorageRepositoryProvider =
|
||||||
Provider((ref) => SecureStorageRepository(const FlutterSecureStorage()));
|
Provider((ref) => const SecureStorageRepository(FlutterSecureStorage()));
|
||||||
|
|
||||||
class SecureStorageRepository {
|
class SecureStorageRepository {
|
||||||
final FlutterSecureStorage _secureStorage;
|
final FlutterSecureStorage _secureStorage;
|
||||||
|
|
||||||
SecureStorageRepository(this._secureStorage);
|
const SecureStorageRepository(this._secureStorage);
|
||||||
|
|
||||||
Future<String?> read(String key) {
|
Future<String?> read(String key) {
|
||||||
return _secureStorage.read(key: key);
|
return _secureStorage.read(key: key);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ final timelineRepositoryProvider =
|
|||||||
Provider((ref) => TimelineRepository(ref.watch(dbProvider)));
|
Provider((ref) => TimelineRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
class TimelineRepository extends DatabaseRepository {
|
class TimelineRepository extends DatabaseRepository {
|
||||||
TimelineRepository(super.db);
|
const TimelineRepository(super.db);
|
||||||
|
|
||||||
Future<List<String>> getTimelineUserIds(String id) {
|
Future<List<String>> getTimelineUserIds(String id) {
|
||||||
return db.users
|
return db.users
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:home_widget/home_widget.dart';
|
import 'package:home_widget/home_widget.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
final widgetRepositoryProvider = Provider((_) => WidgetRepository());
|
final widgetRepositoryProvider = Provider((_) => const WidgetRepository());
|
||||||
|
|
||||||
class WidgetRepository {
|
class WidgetRepository {
|
||||||
WidgetRepository();
|
const WidgetRepository();
|
||||||
|
|
||||||
Future<void> saveData(String key, String value) async {
|
Future<void> saveData(String key, String value) async {
|
||||||
await HomeWidget.saveWidgetData<String>(key, value);
|
await HomeWidget.saveWidgetData<String>(key, value);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
class BackupPermissionGuard extends AutoRouteGuard {
|
class BackupPermissionGuard extends AutoRouteGuard {
|
||||||
final GalleryPermissionNotifier _permission;
|
final GalleryPermissionNotifier _permission;
|
||||||
|
|
||||||
BackupPermissionGuard(this._permission);
|
const BackupPermissionGuard(this._permission);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
|
|
||||||
/// Guards against duplicate navigation to this route
|
/// Guards against duplicate navigation to this route
|
||||||
class DuplicateGuard extends AutoRouteGuard {
|
class DuplicateGuard extends AutoRouteGuard {
|
||||||
DuplicateGuard();
|
const DuplicateGuard();
|
||||||
@override
|
@override
|
||||||
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
||||||
// Duplicate navigation
|
// Duplicate navigation
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
|||||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/tab_shell.page.dart';
|
||||||
import 'package:immich_mobile/pages/editing/crop.page.dart';
|
import 'package:immich_mobile/pages/editing/crop.page.dart';
|
||||||
import 'package:immich_mobile/pages/editing/edit.page.dart';
|
import 'package:immich_mobile/pages/editing/edit.page.dart';
|
||||||
import 'package:immich_mobile/pages/editing/filter.page.dart';
|
import 'package:immich_mobile/pages/editing/filter.page.dart';
|
||||||
@@ -105,7 +106,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
LocalAuthService localAuthService,
|
LocalAuthService localAuthService,
|
||||||
) {
|
) {
|
||||||
_authGuard = AuthGuard(apiService);
|
_authGuard = AuthGuard(apiService);
|
||||||
_duplicateGuard = DuplicateGuard();
|
_duplicateGuard = const DuplicateGuard();
|
||||||
_lockedGuard =
|
_lockedGuard =
|
||||||
LockedGuard(apiService, secureStorageService, localAuthService);
|
LockedGuard(apiService, secureStorageService, localAuthService);
|
||||||
_backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier);
|
_backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier);
|
||||||
@@ -152,6 +153,30 @@ class AppRouter extends RootStackRouter {
|
|||||||
],
|
],
|
||||||
transitionsBuilder: TransitionsBuilders.fadeIn,
|
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||||
),
|
),
|
||||||
|
CustomRoute(
|
||||||
|
page: TabShellRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
children: [
|
||||||
|
AutoRoute(
|
||||||
|
page: MainTimelineRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: SearchRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
maintainState: false,
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: LibraryRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: AlbumsRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
transitionsBuilder: TransitionsBuilders.fadeIn,
|
||||||
|
),
|
||||||
CustomRoute(
|
CustomRoute(
|
||||||
page: GalleryViewerRoute.page,
|
page: GalleryViewerRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
|||||||
@@ -1662,6 +1662,22 @@ class TabControllerRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [TabShellPage]
|
||||||
|
class TabShellRoute extends PageRouteInfo<void> {
|
||||||
|
const TabShellRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(TabShellRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'TabShellRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const TabShellPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [TrashPage]
|
/// [TrashPage]
|
||||||
class TrashRoute extends PageRouteInfo<void> {
|
class TrashRoute extends PageRouteInfo<void> {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ enum AppSettingsEnum<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppSettingsService {
|
class AppSettingsService {
|
||||||
|
const AppSettingsService();
|
||||||
T getSetting<T>(AppSettingsEnum<T> setting) {
|
T getSetting<T>(AppSettingsEnum<T> setting) {
|
||||||
return Store.get(setting.storeKey, setting.defaultValue);
|
return Store.get(setting.storeKey, setting.defaultValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ final backupAlbumServiceProvider = Provider<BackupAlbumService>((ref) {
|
|||||||
class BackupAlbumService {
|
class BackupAlbumService {
|
||||||
final BackupAlbumRepository _backupAlbumRepository;
|
final BackupAlbumRepository _backupAlbumRepository;
|
||||||
|
|
||||||
BackupAlbumService(this._backupAlbumRepository);
|
const BackupAlbumService(this._backupAlbumRepository);
|
||||||
|
|
||||||
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
||||||
return _backupAlbumRepository.getAll(sort: sort);
|
return _backupAlbumRepository.getAll(sort: sort);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class DeepLinkService {
|
|||||||
final CurrentAsset _currentAsset;
|
final CurrentAsset _currentAsset;
|
||||||
final CurrentAlbum _currentAlbum;
|
final CurrentAlbum _currentAlbum;
|
||||||
|
|
||||||
DeepLinkService(
|
const DeepLinkService(
|
||||||
this._memoryService,
|
this._memoryService,
|
||||||
this._assetService,
|
this._assetService,
|
||||||
this._albumService,
|
this._albumService,
|
||||||
@@ -38,16 +38,12 @@ class DeepLinkService {
|
|||||||
final intent = link.uri.host;
|
final intent = link.uri.host;
|
||||||
final queryParams = link.uri.queryParameters;
|
final queryParams = link.uri.queryParameters;
|
||||||
|
|
||||||
PageRouteInfo<dynamic>? deepLinkRoute;
|
PageRouteInfo<dynamic>? deepLinkRoute = switch (intent) {
|
||||||
|
"memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''),
|
||||||
switch (intent) {
|
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? ''),
|
||||||
case "memory":
|
"album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''),
|
||||||
deepLinkRoute = await _buildMemoryDeepLink(queryParams['id'] ?? '');
|
_ => null,
|
||||||
case "asset":
|
};
|
||||||
deepLinkRoute = await _buildAssetDeepLink(queryParams['id'] ?? '');
|
|
||||||
case "album":
|
|
||||||
deepLinkRoute = await _buildAlbumDeepLink(queryParams['id'] ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deep link resolution failed, safely handle it based on the app state
|
// Deep link resolution failed, safely handle it based on the app state
|
||||||
if (deepLinkRoute == null) {
|
if (deepLinkRoute == null) {
|
||||||
@@ -98,7 +94,7 @@ class DeepLinkService {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MemoryRoute?> _buildMemoryDeepLink(String memoryId) async {
|
Future<PageRouteInfo?> _buildMemoryDeepLink(String memoryId) async {
|
||||||
final memory = await _memoryService.getMemoryById(memoryId);
|
final memory = await _memoryService.getMemoryById(memoryId);
|
||||||
|
|
||||||
if (memory == null) {
|
if (memory == null) {
|
||||||
@@ -108,7 +104,7 @@ class DeepLinkService {
|
|||||||
return MemoryRoute(memories: [memory], memoryIndex: 0);
|
return MemoryRoute(memories: [memory], memoryIndex: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GalleryViewerRoute?> _buildAssetDeepLink(String assetId) async {
|
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId) async {
|
||||||
final asset = await _assetService.getAssetByRemoteId(assetId);
|
final asset = await _assetService.getAssetByRemoteId(assetId);
|
||||||
|
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
@@ -126,7 +122,7 @@ class DeepLinkService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AlbumViewerRoute?> _buildAlbumDeepLink(String albumId) async {
|
Future<PageRouteInfo?> _buildAlbumDeepLink(String albumId) async {
|
||||||
final album = await _albumService.getAlbumByRemoteId(albumId);
|
final album = await _albumService.getAlbumByRemoteId(albumId);
|
||||||
|
|
||||||
if (album == null) {
|
if (album == null) {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
|
||||||
final deviceServiceProvider = Provider((ref) => DeviceService());
|
final deviceServiceProvider = Provider((ref) => const DeviceService());
|
||||||
|
|
||||||
class DeviceService {
|
class DeviceService {
|
||||||
DeviceService();
|
const DeviceService();
|
||||||
|
|
||||||
createDeviceId() {
|
createDeviceId() {
|
||||||
return FlutterUdid.consistentUdid;
|
return FlutterUdid.consistentUdid;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
|
|||||||
class EntityService {
|
class EntityService {
|
||||||
final AssetRepository _assetRepository;
|
final AssetRepository _assetRepository;
|
||||||
final IsarUserRepository _isarUserRepository;
|
final IsarUserRepository _isarUserRepository;
|
||||||
EntityService(
|
const EntityService(
|
||||||
this._assetRepository,
|
this._assetRepository,
|
||||||
this._isarUserRepository,
|
this._isarUserRepository,
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user