chore: pull main

This commit is contained in:
shalong-tanwen 2023-11-10 23:16:36 +05:30
commit d2669fc906
313 changed files with 9083 additions and 11860 deletions

View file

@ -38,7 +38,7 @@ jobs:
- -
name: Clean temporary images name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0 uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "immich-app" owner: "immich-app"
@ -70,7 +70,7 @@ jobs:
- -
name: Clean untagged images name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.3.0 uses: stumpylog/image-cleaner-action/untagged@v0.4.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "immich-app" owner: "immich-app"

View file

@ -33,91 +33,10 @@ jobs:
- context: "nginx" - context: "nginx"
image: "immich-proxy" image: "immich-proxy"
platforms: "linux/amd64,linux/arm64" platforms: "linux/amd64,linux/arm64"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761
with:
driver-opts: |
image=moby/buildkit:v0.10.6
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
latest=false
images: |
name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Determine build cache output
id: cache-target
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
fi
- name: Build and push image
uses: docker/build-push-action@v5.0.0
with:
context: ${{ matrix.context }}
platforms: ${{ matrix.platforms }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
build_and_push_server_arm_64:
runs-on: self-hosted
strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false
matrix:
include:
- context: "server" - context: "server"
image: "immich-server" image: "immich-server"
platforms: "linux/arm64,linux/amd64" platforms: "linux/arm64,linux/amd64"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View file

@ -2,7 +2,7 @@
<br/> <br/>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a> <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
<a href="https://discord.gg/D8JsnBEuKb"> <a href="https://discord.gg/D8JsnBEuKb">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
<br/> <br/>

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.84.0 * The version of the OpenAPI document: 1.85.0
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -209,43 +209,6 @@ export interface AddUsersDto {
*/ */
'sharedUserIds': Array<string>; 'sharedUserIds': Array<string>;
} }
/**
*
* @export
* @interface AdminSignupResponseDto
*/
export interface AdminSignupResponseDto {
/**
*
* @type {string}
* @memberof AdminSignupResponseDto
*/
'createdAt': string;
/**
*
* @type {string}
* @memberof AdminSignupResponseDto
*/
'email': string;
/**
*
* @type {string}
* @memberof AdminSignupResponseDto
*/
'firstName': string;
/**
*
* @type {string}
* @memberof AdminSignupResponseDto
*/
'id': string;
/**
*
* @type {string}
* @memberof AdminSignupResponseDto
*/
'lastName': string;
}
/** /**
* *
* @export * @export
@ -331,6 +294,12 @@ export interface AlbumResponseDto {
* @memberof AlbumResponseDto * @memberof AlbumResponseDto
*/ */
'id': string; 'id': string;
/**
*
* @type {boolean}
* @memberof AlbumResponseDto
*/
'isActivityEnabled': boolean;
/** /**
* *
* @type {string} * @type {string}
@ -1855,7 +1824,7 @@ export interface ImportAssetDto {
* @type {boolean} * @type {boolean}
* @memberof ImportAssetDto * @memberof ImportAssetDto
*/ */
'isFavorite': boolean; 'isFavorite'?: boolean;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -2255,6 +2224,20 @@ export interface MapMarkerResponseDto {
*/ */
'lon': number; 'lon': number;
} }
/**
*
* @export
* @enum {string}
*/
export const MapTheme = {
Light: 'light',
Dark: 'dark'
} as const;
export type MapTheme = typeof MapTheme[keyof typeof MapTheme];
/** /**
* *
* @export * @export
@ -2587,6 +2570,20 @@ export interface QueueStatusDto {
*/ */
'isPaused': boolean; 'isPaused': boolean;
} }
/**
*
* @export
* @enum {string}
*/
export const ReactionLevel = {
Album: 'album',
Asset: 'asset'
} as const;
export type ReactionLevel = typeof ReactionLevel[keyof typeof ReactionLevel];
/** /**
* *
* @export * @export
@ -2853,12 +2850,6 @@ export interface ServerConfigDto {
* @memberof ServerConfigDto * @memberof ServerConfigDto
*/ */
'loginPageMessage': string; 'loginPageMessage': string;
/**
*
* @type {string}
* @memberof ServerConfigDto
*/
'mapTileUrl': string;
/** /**
* *
* @type {string} * @type {string}
@ -3726,6 +3717,12 @@ export interface SystemConfigMachineLearningDto {
* @interface SystemConfigMapDto * @interface SystemConfigMapDto
*/ */
export interface SystemConfigMapDto { export interface SystemConfigMapDto {
/**
*
* @type {string}
* @memberof SystemConfigMapDto
*/
'darkStyle': string;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -3737,7 +3734,7 @@ export interface SystemConfigMapDto {
* @type {string} * @type {string}
* @memberof SystemConfigMapDto * @memberof SystemConfigMapDto
*/ */
'tileUrl': string; 'lightStyle': string;
} }
/** /**
* *
@ -4160,6 +4157,12 @@ export interface UpdateAlbumDto {
* @memberof UpdateAlbumDto * @memberof UpdateAlbumDto
*/ */
'description'?: string; 'description'?: string;
/**
*
* @type {boolean}
* @memberof UpdateAlbumDto
*/
'isActivityEnabled'?: boolean;
} }
/** /**
* *
@ -5076,11 +5079,12 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
* @param {string} albumId * @param {string} albumId
* @param {string} [assetId] * @param {string} [assetId]
* @param {ReactionType} [type] * @param {ReactionType} [type]
* @param {ReactionLevel} [level]
* @param {string} [userId] * @param {string} [userId]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getActivities: async (albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'albumId' is not null or undefined // verify required parameter 'albumId' is not null or undefined
assertParamExists('getActivities', 'albumId', albumId) assertParamExists('getActivities', 'albumId', albumId)
const localVarPath = `/activity`; const localVarPath = `/activity`;
@ -5116,6 +5120,10 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
localVarQueryParameter['type'] = type; localVarQueryParameter['type'] = type;
} }
if (level !== undefined) {
localVarQueryParameter['level'] = level;
}
if (userId !== undefined) { if (userId !== undefined) {
localVarQueryParameter['userId'] = userId; localVarQueryParameter['userId'] = userId;
} }
@ -5216,12 +5224,13 @@ export const ActivityApiFp = function(configuration?: Configuration) {
* @param {string} albumId * @param {string} albumId
* @param {string} [assetId] * @param {string} [assetId]
* @param {ReactionType} [type] * @param {ReactionType} [type]
* @param {ReactionLevel} [level]
* @param {string} [userId] * @param {string} [userId]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> { async getActivities(albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, level, userId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -5270,7 +5279,7 @@ export const ActivityApiFactory = function (configuration?: Configuration, baseP
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> { getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath)); return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@ -5339,6 +5348,13 @@ export interface ActivityApiGetActivitiesRequest {
*/ */
readonly type?: ReactionType readonly type?: ReactionType
/**
*
* @type {ReactionLevel}
* @memberof ActivityApiGetActivities
*/
readonly level?: ReactionLevel
/** /**
* *
* @type {string} * @type {string}
@ -5405,7 +5421,7 @@ export class ActivityApi extends BaseAPI {
* @memberof ActivityApi * @memberof ActivityApi
*/ */
public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) { public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@ -6695,16 +6711,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
}, },
/** /**
* Get all AssetEntity belong to the user * Get all AssetEntity belong to the user
* @param {number} [skip]
* @param {number} [take]
* @param {string} [userId] * @param {string} [userId]
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived] * @param {boolean} [isArchived]
* @param {number} [skip]
* @param {string} [updatedAfter] * @param {string} [updatedAfter]
* @param {string} [updatedBefore]
* @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {string} [ifNoneMatch] ETag of data already cached on the client
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getAllAssets: async (skip?: number, take?: number, userId?: string, isFavorite?: boolean, isArchived?: boolean, updatedAfter?: string, updatedBefore?: string, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset`; const localVarPath = `/asset`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -6726,6 +6744,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration) await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (skip !== undefined) {
localVarQueryParameter['skip'] = skip;
}
if (take !== undefined) {
localVarQueryParameter['take'] = take;
}
if (userId !== undefined) { if (userId !== undefined) {
localVarQueryParameter['userId'] = userId; localVarQueryParameter['userId'] = userId;
} }
@ -6738,16 +6764,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['isArchived'] = isArchived; localVarQueryParameter['isArchived'] = isArchived;
} }
if (skip !== undefined) {
localVarQueryParameter['skip'] = skip;
}
if (updatedAfter !== undefined) { if (updatedAfter !== undefined) {
localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ?
(updatedAfter as any).toISOString() : (updatedAfter as any).toISOString() :
updatedAfter; updatedAfter;
} }
if (updatedBefore !== undefined) {
localVarQueryParameter['updatedBefore'] = (updatedBefore as any instanceof Date) ?
(updatedBefore as any).toISOString() :
updatedBefore;
}
if (ifNoneMatch != null) { if (ifNoneMatch != null) {
localVarHeaderParameter['if-none-match'] = String(ifNoneMatch); localVarHeaderParameter['if-none-match'] = String(ifNoneMatch);
} }
@ -7868,11 +7896,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {string} deviceId * @param {string} deviceId
* @param {string} fileCreatedAt * @param {string} fileCreatedAt
* @param {string} fileModifiedAt * @param {string} fileModifiedAt
* @param {boolean} isFavorite
* @param {string} [key] * @param {string} [key]
* @param {string} [duration] * @param {string} [duration]
* @param {boolean} [isArchived] * @param {boolean} [isArchived]
* @param {boolean} [isExternal] * @param {boolean} [isExternal]
* @param {boolean} [isFavorite]
* @param {boolean} [isOffline] * @param {boolean} [isOffline]
* @param {boolean} [isReadOnly] * @param {boolean} [isReadOnly]
* @param {boolean} [isVisible] * @param {boolean} [isVisible]
@ -7882,7 +7910,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
uploadFile: async (assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, duration?: string, isArchived?: boolean, isExternal?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, libraryId?: string, livePhotoData?: File, sidecarData?: File, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { uploadFile: async (assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, key?: string, duration?: string, isArchived?: boolean, isExternal?: boolean, isFavorite?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, libraryId?: string, livePhotoData?: File, sidecarData?: File, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetData' is not null or undefined // verify required parameter 'assetData' is not null or undefined
assertParamExists('uploadFile', 'assetData', assetData) assertParamExists('uploadFile', 'assetData', assetData)
// verify required parameter 'deviceAssetId' is not null or undefined // verify required parameter 'deviceAssetId' is not null or undefined
@ -7893,8 +7921,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
assertParamExists('uploadFile', 'fileCreatedAt', fileCreatedAt) assertParamExists('uploadFile', 'fileCreatedAt', fileCreatedAt)
// verify required parameter 'fileModifiedAt' is not null or undefined // verify required parameter 'fileModifiedAt' is not null or undefined
assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt) assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt)
// verify required parameter 'isFavorite' is not null or undefined
assertParamExists('uploadFile', 'isFavorite', isFavorite)
const localVarPath = `/asset/upload`; const localVarPath = `/asset/upload`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -8068,17 +8094,19 @@ export const AssetApiFp = function(configuration?: Configuration) {
}, },
/** /**
* Get all AssetEntity belong to the user * Get all AssetEntity belong to the user
* @param {number} [skip]
* @param {number} [take]
* @param {string} [userId] * @param {string} [userId]
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isArchived] * @param {boolean} [isArchived]
* @param {number} [skip]
* @param {string} [updatedAfter] * @param {string} [updatedAfter]
* @param {string} [updatedBefore]
* @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {string} [ifNoneMatch] ETag of data already cached on the client
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { async getAllAssets(skip?: number, take?: number, userId?: string, isFavorite?: boolean, isArchived?: boolean, updatedAfter?: string, updatedBefore?: string, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, skip, updatedAfter, ifNoneMatch, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(skip, take, userId, isFavorite, isArchived, updatedAfter, updatedBefore, ifNoneMatch, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -8335,11 +8363,11 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {string} deviceId * @param {string} deviceId
* @param {string} fileCreatedAt * @param {string} fileCreatedAt
* @param {string} fileModifiedAt * @param {string} fileModifiedAt
* @param {boolean} isFavorite
* @param {string} [key] * @param {string} [key]
* @param {string} [duration] * @param {string} [duration]
* @param {boolean} [isArchived] * @param {boolean} [isArchived]
* @param {boolean} [isExternal] * @param {boolean} [isExternal]
* @param {boolean} [isFavorite]
* @param {boolean} [isOffline] * @param {boolean} [isOffline]
* @param {boolean} [isReadOnly] * @param {boolean} [isReadOnly]
* @param {boolean} [isVisible] * @param {boolean} [isVisible]
@ -8349,8 +8377,8 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async uploadFile(assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, duration?: string, isArchived?: boolean, isExternal?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, libraryId?: string, livePhotoData?: File, sidecarData?: File, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> { async uploadFile(assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, key?: string, duration?: string, isArchived?: boolean, isExternal?: boolean, isFavorite?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, libraryId?: string, livePhotoData?: File, sidecarData?: File, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, duration, isArchived, isExternal, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData, options); const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key, duration, isArchived, isExternal, isFavorite, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
} }
@ -8423,7 +8451,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); return localVarFp.getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath));
}, },
/** /**
* Get a single asset\'s information * Get a single asset\'s information
@ -8626,7 +8654,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError} * @throws {RequiredError}
*/ */
uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> { uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> {
return localVarFp.uploadFile(requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.duration, requestParameters.isArchived, requestParameters.isExternal, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.libraryId, requestParameters.livePhotoData, requestParameters.sidecarData, options).then((request) => request(axios, basePath)); return localVarFp.uploadFile(requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.key, requestParameters.duration, requestParameters.isArchived, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.libraryId, requestParameters.livePhotoData, requestParameters.sidecarData, options).then((request) => request(axios, basePath));
}, },
}; };
}; };
@ -8721,6 +8749,20 @@ export interface AssetApiDownloadFileRequest {
* @interface AssetApiGetAllAssetsRequest * @interface AssetApiGetAllAssetsRequest
*/ */
export interface AssetApiGetAllAssetsRequest { export interface AssetApiGetAllAssetsRequest {
/**
*
* @type {number}
* @memberof AssetApiGetAllAssets
*/
readonly skip?: number
/**
*
* @type {number}
* @memberof AssetApiGetAllAssets
*/
readonly take?: number
/** /**
* *
* @type {string} * @type {string}
@ -8744,17 +8786,17 @@ export interface AssetApiGetAllAssetsRequest {
/** /**
* *
* @type {number} * @type {string}
* @memberof AssetApiGetAllAssets * @memberof AssetApiGetAllAssets
*/ */
readonly skip?: number readonly updatedAfter?: string
/** /**
* *
* @type {string} * @type {string}
* @memberof AssetApiGetAllAssets * @memberof AssetApiGetAllAssets
*/ */
readonly updatedAfter?: string readonly updatedBefore?: string
/** /**
* ETag of data already cached on the client * ETag of data already cached on the client
@ -9274,13 +9316,6 @@ export interface AssetApiUploadFileRequest {
*/ */
readonly fileModifiedAt: string readonly fileModifiedAt: string
/**
*
* @type {boolean}
* @memberof AssetApiUploadFile
*/
readonly isFavorite: boolean
/** /**
* *
* @type {string} * @type {string}
@ -9309,6 +9344,13 @@ export interface AssetApiUploadFileRequest {
*/ */
readonly isExternal?: boolean readonly isExternal?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiUploadFile
*/
readonly isFavorite?: boolean
/** /**
* *
* @type {boolean} * @type {boolean}
@ -9432,7 +9474,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi * @memberof AssetApi
*/ */
public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) { public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath));
} }
/** /**
@ -9681,7 +9723,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi * @memberof AssetApi
*/ */
public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) { public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).uploadFile(requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.duration, requestParameters.isArchived, requestParameters.isExternal, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.libraryId, requestParameters.livePhotoData, requestParameters.sidecarData, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).uploadFile(requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.key, requestParameters.duration, requestParameters.isArchived, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.libraryId, requestParameters.livePhotoData, requestParameters.sidecarData, options).then((request) => request(this.axios, this.basePath));
} }
} }
@ -10471,7 +10513,7 @@ export const AuthenticationApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AdminSignupResponseDto>> { async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.signUpAdmin(signUpDto, options); const localVarAxiosArgs = await localVarAxiosParamCreator.signUpAdmin(signUpDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@ -10551,7 +10593,7 @@ export const AuthenticationApiFactory = function (configuration?: Configuration,
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise<AdminSignupResponseDto> { signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
return localVarFp.signUpAdmin(requestParameters.signUpDto, options).then((request) => request(axios, basePath)); return localVarFp.signUpAdmin(requestParameters.signUpDto, options).then((request) => request(axios, basePath));
}, },
/** /**
@ -15062,6 +15104,51 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {MapTheme} theme
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMapStyle: async (theme: MapTheme, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'theme' is not null or undefined
assertParamExists('getMapStyle', 'theme', theme)
const localVarPath = `/system-config/map/style.json`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (theme !== undefined) {
localVarQueryParameter['theme'] = theme;
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -15181,6 +15268,16 @@ export const SystemConfigApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getConfigDefaults(options); const localVarAxiosArgs = await localVarAxiosParamCreator.getConfigDefaults(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {MapTheme} theme
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getMapStyle(theme: MapTheme, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMapStyle(theme, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -15226,6 +15323,15 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b
getConfigDefaults(options?: AxiosRequestConfig): AxiosPromise<SystemConfigDto> { getConfigDefaults(options?: AxiosRequestConfig): AxiosPromise<SystemConfigDto> {
return localVarFp.getConfigDefaults(options).then((request) => request(axios, basePath)); return localVarFp.getConfigDefaults(options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig): AxiosPromise<object> {
return localVarFp.getMapStyle(requestParameters.theme, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -15246,6 +15352,20 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b
}; };
}; };
/**
* Request parameters for getMapStyle operation in SystemConfigApi.
* @export
* @interface SystemConfigApiGetMapStyleRequest
*/
export interface SystemConfigApiGetMapStyleRequest {
/**
*
* @type {MapTheme}
* @memberof SystemConfigApiGetMapStyle
*/
readonly theme: MapTheme
}
/** /**
* Request parameters for updateConfig operation in SystemConfigApi. * Request parameters for updateConfig operation in SystemConfigApi.
* @export * @export
@ -15287,6 +15407,17 @@ export class SystemConfigApi extends BaseAPI {
return SystemConfigApiFp(this.configuration).getConfigDefaults(options).then((request) => request(this.axios, this.basePath)); return SystemConfigApiFp(this.configuration).getConfigDefaults(options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SystemConfigApi
*/
public getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig) {
return SystemConfigApiFp(this.configuration).getMapStyle(requestParameters.theme, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.84.0 * The version of the OpenAPI document: 1.85.0
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.84.0 * The version of the OpenAPI document: 1.85.0
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.84.0 * The version of the OpenAPI document: 1.85.0
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.84.0 * The version of the OpenAPI document: 1.85.0
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -1,5 +1,11 @@
# See:
# - https://immich.app/docs/developer/setup
# - https://immich.app/docs/developer/troubleshooting
version: "3.8" version: "3.8"
name: immich-dev
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
@ -71,10 +77,6 @@ services:
command: npm run dev --host command: npm run dev --host
env_file: env_file:
- .env - .env
environment:
# Rename these values for svelte public interface
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
ports: ports:
- 3000:3000 - 3000:3000
- 24678:24678 - 24678:24678

View file

@ -1,5 +1,7 @@
version: "3.8" version: "3.8"
name: immich-prod
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
@ -9,7 +11,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
command: [ "./start-server.sh" ] command: [ "./start-server.sh" ]
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
@ -18,19 +20,6 @@ services:
- database - database
- typesense - typesense
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- model-cache:/cache
env_file:
- .env
restart: always
immich-microservices: immich-microservices:
container_name: immich_microservices container_name: immich_microservices
image: immich-microservices:latest image: immich-microservices:latest
@ -42,7 +31,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
command: [ "./start-microservices.sh" ] command: [ "./start-microservices.sh" ]
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
@ -64,6 +53,18 @@ services:
depends_on: depends_on:
- immich-server - immich-server
immich-machine-learning:
container_name: immich_machine_learning
image: immich-machine-learning:latest
build:
context: ../machine-learning
dockerfile: Dockerfile
volumes:
- model-cache:/cache
env_file:
- .env
restart: always
typesense: typesense:
container_name: immich_typesense container_name: immich_typesense
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
@ -73,7 +74,7 @@ services:
# remove this to get debug messages # remove this to get debug messages
- GLOG_minloglevel=1 - GLOG_minloglevel=1
volumes: volumes:
- tsdata:/data - ${UPLOAD_LOCATION}/typesense:/data
restart: always restart: always
redis: redis:
@ -91,7 +92,7 @@ services:
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_DB: ${DB_DATABASE_NAME}
volumes: volumes:
- pgdata:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
restart: always restart: always
immich-proxy: immich-proxy:
@ -113,6 +114,4 @@ services:
restart: always restart: always
volumes: volumes:
pgdata:
model-cache: model-cache:
tsdata:

View file

@ -1,5 +1,7 @@
version: "3.8" version: "3.8"
name: immich
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server

View file

@ -9,6 +9,6 @@ npm run typeorm:migrations:generate ./src/infra/<migration-name>
``` ```
2. Check if the migration file makes sense. 2. Check if the migration file makes sense.
3. Move the migration file to folder `./src/infra/database/migrations` in your code editor. 3. Move the migration file to folder `./server/src/infra/migrations` in your code editor.
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately. The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.

View file

@ -0,0 +1,19 @@
# Troubleshooting
:::tip
A great option to get assistance with troubleshooting is to join our [Discord](https://discord.gg/D8JsnBEuKb) server, where we have a dedicated channel for `#contributing`.
:::
## Known Issues
### Running on Windows
Running Immich on Windows can be frustrating and there are lots of ways it can go wrong. Where possible we recommend using Docker on Linux. However, several people have had success running Immich on Windows using Docker via WSL2.
### NTFS Mounted Volumes
The docker-compose.dev.yml and docker-compose.prod.yml use volume mounts for the postgres database. On start-up, postgres will try to `chown` the data directory, but fail. See [this post](https://forums.docker.com/t/data-directory-var-lib-postgresql-data-pgdata-has-wrong-ownership/17963/24) for more information about this issue and possible solutions.
### `Cannot read properties of null (reading 'split')`
This error occurs when trying to access the app via port `3000` instead of `2283`. During development `immich-proxy` runs on port 2283, while `immich-web` runs on `3000`.

View file

@ -4,6 +4,10 @@ You can use the CLI to upload an existing gallery to the Immich server
[Immich CLI Repository](https://github.com/immich-app/CLI) [Immich CLI Repository](https://github.com/immich-app/CLI)
:::tip Google Photos Takeout
If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
:::
## Requirements ## Requirements
- Node.js 16 or above - Node.js 16 or above

View file

@ -1,5 +1,7 @@
# Facial Recognition # Facial Recognition
## Overview
Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them. Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them.
The list of people is shown in the Explore page. The list of people is shown in the Explore page.
@ -13,3 +15,16 @@ Upon clicking on a person, a list of assets that contain their face will be show
The asset detail view will also show the faces that are recognized in the asset. The asset detail view will also show the faces that are recognized in the asset.
<img src={require('./img/facial-recognition-3.png').default} title='Facial Recognition 3' /> <img src={require('./img/facial-recognition-3.png').default} title='Facial Recognition 3' />
## Actions
Additional actions you can do with a detected person are:
- Change the feature face photo of the person
- Set date of birth
- Merge two or more detected faces into one person
- Hide face
It can be found from the app bar when you access the detial view of a person
<img src={require('./img/facial-recognition-4.png').default} title='Facial Recognition 4' width="70%"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

View file

@ -1,6 +1,6 @@
import MobileAppDownload from '../partials/_mobile-app-download.md'; import MobileAppDownload from '../partials/_mobile-app-download.md';
import MobileAppLogin from '../partials/_mobile-app-login.md'; import MobileAppLogin from '../partials/_mobile-app-login.md';
import MobileAppBackup from '../partials/_mobile-app-login.md'; import MobileAppBackup from '../partials/_mobile-app-backup.md';
# Mobile App # Mobile App

View file

@ -0,0 +1,42 @@
# Python File Upload
```python
#!/usr/bin/python3
import requests
import os
from datetime import datetime
API_KEY = 'YOUR_API_KEY' # replace with a valid api key
BASE_URL = 'http://127.0.0.1:2283/api' # replace as needed
def upload(file):
stats = os.stat(file)
headers = {
'Accept': 'application/json',
'x-api-key': API_KEY
}
data = {
'deviceAssetId': f'{file}-{stats.st_mtime}',
'deviceId': 'python',
'fileCreatedAt': datetime.fromtimestamp(stats.st_mtime),
'fileModifiedAt': datetime.fromtimestamp(stats.st_mtime),
'isFavorite': 'false',
}
files = {
'assetData': open(file, 'rb')
}
response = requests.post(
f'{BASE_URL}/asset/upload', headers=headers, data=data, files=files)
print(response.json())
# {'id': 'ef96f635-61c7-4639-9e60-61a11c4bbfba', 'duplicate': False}
upload('./test.jpg')
```

View file

@ -17,6 +17,12 @@ The default configuration looks like this:
"targetAudioCodec": "aac", "targetAudioCodec": "aac",
"targetResolution": "720", "targetResolution": "720",
"maxBitrate": "0", "maxBitrate": "0",
"bframes": -1,
"refs": 0,
"gopSize": 0,
"npl": 0,
"temporalAQ": false,
"cqMode": "auto",
"twoPass": false, "twoPass": false,
"transcode": "required", "transcode": "required",
"tonemap": "hable", "tonemap": "hable",
@ -44,9 +50,15 @@ The default configuration looks like this:
"sidecar": { "sidecar": {
"concurrency": 5 "concurrency": 5
}, },
"library": {
"concurrency": 5
},
"storageTemplateMigration": { "storageTemplateMigration": {
"concurrency": 5 "concurrency": 5
}, },
"migration": {
"concurrency": 5
},
"thumbnailGeneration": { "thumbnailGeneration": {
"concurrency": 5 "concurrency": 5
}, },
@ -55,16 +67,16 @@ The default configuration looks like this:
} }
}, },
"machineLearning": { "machineLearning": {
"classification": {
"minScore": 0.7,
"enabled": true,
"modelName": "microsoft/resnet-50"
},
"enabled": true, "enabled": true,
"url": "http://immich-machine-learning:3003", "url": "http://immich-machine-learning:3003",
"classification": {
"enabled": true,
"modelName": "microsoft/resnet-50",
"minScore": 0.9
},
"clip": { "clip": {
"enabled": true, "enabled": true,
"modelName": "ViT-B-32::openai" "modelName": "ViT-B-32__openai"
}, },
"facialRecognition": { "facialRecognition": {
"enabled": true, "enabled": true,
@ -74,6 +86,14 @@ The default configuration looks like this:
"minFaces": 1 "minFaces": 1
} }
}, },
"map": {
"enabled": true,
"tileUrl": "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
},
"reverseGeocoding": {
"enabled": true,
"citiesFileOverride": "cities500"
},
"oauth": { "oauth": {
"enabled": false, "enabled": false,
"issuerUrl": "", "issuerUrl": "",
@ -96,8 +116,27 @@ The default configuration looks like this:
"thumbnail": { "thumbnail": {
"webpSize": 250, "webpSize": 250,
"jpegSize": 1440, "jpegSize": 1440,
"quality": 90, "quality": 80,
"colorspace": "p3" "colorspace": "p3"
},
"newVersionCheck": {
"enabled": true
},
"trash": {
"enabled": true,
"days": 30
},
"theme": {
"customCss": ""
},
"library": {
"scan": {
"enabled": true,
"cronExpression": "0 0 * * *"
}
},
"stylesheets": {
"css": ""
} }
} }
``` ```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 334 KiB

View file

@ -34,7 +34,7 @@ function HomepageHeader() {
</Link> </Link>
</div> </div>
<img src="/img/immich-screenshots.png" alt="logo" /> <img src="/img/immich-screenshots.png" alt="screenshots" width={'85%'} />
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1"> <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
<div className="h-24"> <div className="h-24">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.84.0" version = "1.85.0"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"

View file

@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 108, "android.injected.version.code" => 109,
"android.injected.version.name" => "1.84.0", "android.injected.version.name" => "1.85.0",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View file

@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000625"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000244">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="70.943413"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.0562">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.374484"> <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.087498">
</testcase> </testcase>

View file

@ -320,6 +320,7 @@
"shared_link_edit_description": "Description", "shared_link_edit_description": "Description",
"shared_link_edit_description_hint": "Enter the share description", "shared_link_edit_description_hint": "Enter the share description",
"shared_link_edit_password": "Password", "shared_link_edit_password": "Password",
"shared_link_edit_expire_after": "Expire after",
"shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_password_hint": "Enter the share password",
"shared_link_edit_show_meta": "Show metadata", "shared_link_edit_show_meta": "Show metadata",
"shared_link_edit_submit_button": "Update link", "shared_link_edit_submit_button": "Update link",
@ -377,5 +378,11 @@
"viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_stack_use_as_main_asset": "Use as Main Asset",
"app_bar_signout_dialog_title": "Sign out", "app_bar_signout_dialog_title": "Sign out",
"app_bar_signout_dialog_content": "Are you sure you wanna sign out?", "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
"app_bar_signout_dialog_ok": "Yes" "app_bar_signout_dialog_ok": "Yes",
"shared_album_activities_input_hint": "Say something",
"shared_album_activity_remove_title": "Delete Activity",
"shared_album_activity_remove_content": "Do you want to delete this activity?",
"shared_album_activity_setting_title": "Comments & likes",
"shared_album_activity_setting_subtitle": "Let others respond",
"shared_album_activities_input_disable": "Comment is disabled"
} }

View file

@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 124; CURRENT_PROJECT_VERSION = 125;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 124; CURRENT_PROJECT_VERSION = 125;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 124; CURRENT_PROJECT_VERSION = 125;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View file

@ -59,11 +59,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.84.0</string> <string>1.85.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>124</string> <string>125</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true /> <true />
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View file

@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.84.0" version_number: "1.85.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View file

@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000253"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000291">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.181977"> <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.199372">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="16.12614"> <testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.104477">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.162663"> <testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.164465">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="145.399278"> <testcase classname="fastlane.lanes" name="4: build_app" time="108.828838">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="61.317235"> <testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="60.89387">
</testcase> </testcase>

View file

@ -0,0 +1,54 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
extension ContextHelper on BuildContext {
// Returns the current size from MediaQuery
Size get size => MediaQuery.sizeOf(this);
// Returns the current width from MediaQuery
double get width => size.width;
// Returns the current height from MediaQuery
double get height => size.height;
// Returns true if the app is running on a mobile device (!tablets)
bool get isMobile => width < 550;
// Returns the current ThemeData
ThemeData get themeData => Theme.of(this);
// Returns true if the app is using a dark theme
bool get isDarkTheme => themeData.brightness == Brightness.dark;
// Returns the current Primary color of the Theme
Color get primaryColor => themeData.primaryColor;
// Returns the Scaffold background color of the Theme
Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor;
// Returns the current TextTheme
TextTheme get textTheme => themeData.textTheme;
// Current ColorScheme used
ColorScheme get colorScheme => themeData.colorScheme;
// Pop-out from the current context with optional result
void pop<T>([T? result]) => Navigator.of(this).pop(result);
// Auto-Push new route from the current context
Future<T?> autoPush<T extends Object?>(PageRouteInfo<dynamic> route) =>
AutoRouter.of(this).push(route);
// Auto-Push navigate route from the current context
Future<dynamic> autoNavigate<T extends Object?>(
PageRouteInfo<dynamic> route,
) =>
AutoRouter.of(this).navigate(route);
// Auto-Push replace route from the current context
Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
AutoRouter.of(this).replace(route);
// Auto-Pop from the current context
Future<bool> autoPop<T>([T? result]) => AutoRouter.of(this).pop(result);
}

View file

@ -2,27 +2,6 @@ import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
extension DurationExtension on String {
Duration? toDuration() {
try {
final parts = split(':')
.map((e) => double.parse(e).toInt())
.toList(growable: false);
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
} catch (e) {
return null;
}
}
double toDouble() {
return double.parse(this);
}
int toInt() {
return int.parse(this);
}
}
extension ListExtension<E> on List<E> { extension ListExtension<E> on List<E> {
List<E> uniqueConsecutive({ List<E> uniqueConsecutive({
int Function(E a, E b)? compare, int Function(E a, E b)? compare,

View file

@ -0,0 +1,36 @@
extension TimeAgoExtension on DateTime {
String timeAgo({bool numericDates = true}) {
DateTime date = toLocal();
final date2 = DateTime.now().toLocal();
final difference = date2.difference(date);
if (difference.inSeconds < 5) {
return 'Just now';
} else if (difference.inSeconds < 60) {
return '${difference.inSeconds} seconds ago';
} else if (difference.inMinutes <= 1) {
return (numericDates) ? '1 minute ago' : 'A minute ago';
} else if (difference.inMinutes < 60) {
return '${difference.inMinutes} minutes ago';
} else if (difference.inHours <= 1) {
return (numericDates) ? '1 hour ago' : 'An hour ago';
} else if (difference.inHours < 60) {
return '${difference.inHours} hours ago';
} else if (difference.inDays <= 1) {
return (numericDates) ? '1 day ago' : 'Yesterday';
} else if (difference.inDays < 6) {
return '${difference.inDays} days ago';
} else if ((difference.inDays / 7).ceil() <= 1) {
return (numericDates) ? '1 week ago' : 'Last week';
} else if ((difference.inDays / 7).ceil() < 4) {
return '${(difference.inDays / 7).ceil()} weeks ago';
} else if ((difference.inDays / 30).ceil() <= 1) {
return (numericDates) ? '1 month ago' : 'Last month';
} else if ((difference.inDays / 30).ceil() < 30) {
return '${(difference.inDays / 30).ceil()} months ago';
} else if ((difference.inDays / 365).ceil() <= 1) {
return (numericDates) ? '1 year ago' : 'Last year';
}
return '${(difference.inDays / 365).floor()} years ago';
}
}

View file

@ -0,0 +1,30 @@
extension StringExtension on String {
String capitalize() {
return split(" ")
.map(
(str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1),
)
.join(" ");
}
}
extension DurationExtension on String {
Duration? toDuration() {
try {
final parts = split(':')
.map((e) => double.parse(e).toInt())
.toList(growable: false);
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
} catch (e) {
return null;
}
}
double toDouble() {
return double.parse(this);
}
int toInt() {
return int.parse(this);
}
}

View file

@ -0,0 +1,90 @@
import 'package:immich_mobile/shared/models/user.dart';
import 'package:openapi/api.dart';
enum ActivityType { comment, like }
class Activity {
final String id;
final String? assetId;
final String? comment;
final DateTime createdAt;
final ActivityType type;
final User user;
const Activity({
required this.id,
this.assetId,
this.comment,
required this.createdAt,
required this.type,
required this.user,
});
Activity copyWith({
String? id,
String? assetId,
String? comment,
DateTime? createdAt,
ActivityType? type,
User? user,
}) {
return Activity(
id: id ?? this.id,
assetId: assetId ?? this.assetId,
comment: comment ?? this.comment,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
user: user ?? this.user,
);
}
Activity.fromDto(ActivityResponseDto dto)
: id = dto.id,
assetId = dto.assetId,
comment = dto.comment,
createdAt = dto.createdAt,
type = dto.type == ActivityResponseDtoTypeEnum.comment
? ActivityType.comment
: ActivityType.like,
user = User(
email: dto.user.email,
firstName: dto.user.firstName,
lastName: dto.user.lastName,
profileImagePath: dto.user.profileImagePath,
id: dto.user.id,
// Placeholder values
isAdmin: false,
updatedAt: DateTime.now(),
isPartnerSharedBy: false,
isPartnerSharedWith: false,
memoryEnabled: false,
);
@override
String toString() {
return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Activity &&
other.id == id &&
other.assetId == assetId &&
other.comment == comment &&
other.createdAt == createdAt &&
other.type == type &&
other.user == user;
}
@override
int get hashCode {
return id.hashCode ^
assetId.hashCode ^
comment.hashCode ^
createdAt.hashCode ^
type.hashCode ^
user.hashCode;
}
}

View file

@ -0,0 +1,130 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/modules/activities/services/activity.service.dart';
class ActivityNotifier extends StateNotifier<AsyncValue<List<Activity>>> {
final Ref _ref;
final ActivityService _activityService;
final String albumId;
final String? assetId;
ActivityNotifier(
this._ref,
this._activityService,
this.albumId,
this.assetId,
) : super(
const AsyncData([]),
) {
fetchActivity();
}
Future<void> fetchActivity() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => _activityService.getAllActivities(albumId, assetId),
);
}
Future<void> removeActivity(String id) async {
final activities = state.asData?.value ?? [];
if (await _activityService.removeActivity(id)) {
final removedActivity = activities.firstWhere((a) => a.id == id);
activities.remove(removedActivity);
state = AsyncData(activities);
if (removedActivity.type == ActivityType.comment) {
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: assetId),
).notifier,
)
.removeActivity();
}
}
}
Future<void> addComment(String comment) async {
final activity = await _activityService.addActivity(
albumId,
ActivityType.comment,
assetId: assetId,
comment: comment,
);
if (activity != null) {
final activities = state.asData?.value ?? [];
state = AsyncData([...activities, activity]);
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: assetId),
).notifier,
)
.addActivity();
if (assetId != null) {
// Add a count to the current album's provider as well
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: null),
).notifier,
)
.addActivity();
}
}
}
Future<void> addLike() async {
final activity = await _activityService
.addActivity(albumId, ActivityType.like, assetId: assetId);
if (activity != null) {
final activities = state.asData?.value ?? [];
state = AsyncData([...activities, activity]);
}
}
}
class ActivityStatisticsNotifier extends StateNotifier<int> {
final String albumId;
final String? assetId;
final ActivityService _activityService;
ActivityStatisticsNotifier(this._activityService, this.albumId, this.assetId)
: super(0) {
fetchStatistics();
}
Future<void> fetchStatistics() async {
state = await _activityService.getStatistics(albumId, assetId: assetId);
}
Future<void> addActivity() async {
state = state + 1;
}
Future<void> removeActivity() async {
state = state - 1;
}
}
typedef ActivityParams = ({String albumId, String? assetId});
final activityStateProvider = StateNotifierProvider.autoDispose
.family<ActivityNotifier, AsyncValue<List<Activity>>, ActivityParams>(
(ref, args) {
return ActivityNotifier(
ref,
ref.watch(activityServiceProvider),
args.albumId,
args.assetId,
);
});
final activityStatisticsStateProvider = StateNotifierProvider.autoDispose
.family<ActivityStatisticsNotifier, int, ActivityParams>((ref, args) {
return ActivityStatisticsNotifier(
ref.watch(activityServiceProvider),
args.albumId,
args.assetId,
);
});

View file

@ -0,0 +1,85 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final activityServiceProvider =
Provider((ref) => ActivityService(ref.watch(apiServiceProvider)));
class ActivityService {
final ApiService _apiService;
final Logger _log = Logger("ActivityService");
ActivityService(this._apiService);
Future<List<Activity>> getAllActivities(
String albumId,
String? assetId,
) async {
try {
final list = await _apiService.activityApi
.getActivities(albumId, assetId: assetId);
return list != null ? list.map(Activity.fromDto).toList() : [];
} catch (e) {
_log.severe(
"failed to fetch activities for albumId - $albumId; assetId - $assetId -> $e",
);
rethrow;
}
}
Future<int> getStatistics(String albumId, {String? assetId}) async {
try {
final dto = await _apiService.activityApi
.getActivityStatistics(albumId, assetId: assetId);
return dto?.comments ?? 0;
} catch (e) {
_log.severe(
"failed to fetch activity statistics for albumId - $albumId; assetId - $assetId -> $e",
);
}
return 0;
}
Future<bool> removeActivity(String id) async {
try {
await _apiService.activityApi.deleteActivity(id);
return true;
} catch (e) {
_log.severe(
"failed to remove activity id - $id -> $e",
);
}
return false;
}
Future<Activity?> addActivity(
String albumId,
ActivityType type, {
String? assetId,
String? comment,
}) async {
try {
final dto = await _apiService.activityApi.createActivity(
ActivityCreateDto(
albumId: albumId,
type: type == ActivityType.comment
? ReactionType.comment
: ReactionType.like,
assetId: assetId,
comment: comment,
),
);
if (dto != null) {
return Activity.fromDto(dto);
}
} catch (e) {
_log.severe(
"failed to add activity for albumId - $albumId; assetId - $assetId -> $e",
);
}
return null;
}
}

View file

@ -0,0 +1,317 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
import 'package:immich_mobile/extensions/datetime_extensions.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class ActivitiesPage extends HookConsumerWidget {
final String albumId;
final String? assetId;
final bool withAssetThumbs;
final String appBarTitle;
final bool isOwner;
final bool isReadOnly;
const ActivitiesPage(
this.albumId, {
this.appBarTitle = "",
this.assetId,
this.withAssetThumbs = true,
this.isOwner = false,
this.isReadOnly = false,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider =
activityStateProvider((albumId: albumId, assetId: assetId));
final activities = ref.watch(provider);
final inputController = useTextEditingController();
final inputFocusNode = useFocusNode();
final listViewScrollController = useScrollController();
final currentUser = Store.tryGet(StoreKey.currentUser);
useEffect(
() {
inputFocusNode.requestFocus();
return null;
},
[],
);
buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) {
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
final textStyle = context.textTheme.bodyMedium
?.copyWith(color: textColor.withOpacity(0.6));
return Row(
mainAxisAlignment: leftAlign
? MainAxisAlignment.start
: MainAxisAlignment.spaceBetween,
mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max,
children: [
Text(
"${activity.user.firstName} ${activity.user.lastName}",
style: textStyle,
overflow: TextOverflow.ellipsis,
),
if (leftAlign)
Text(
"",
style: textStyle,
),
Expanded(
child: Text(
activity.createdAt.copyWith().timeAgo(),
style: textStyle,
overflow: TextOverflow.ellipsis,
textAlign: leftAlign ? TextAlign.left : TextAlign.right,
),
),
],
);
}
buildAssetThumbnail(Activity activity) {
return withAssetThumbs && activity.assetId != null
? Container(
width: 40,
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
image: DecorationImage(
image: CachedNetworkImageProvider(
getThumbnailUrlForRemoteId(
activity.assetId!,
),
cacheKey: getThumbnailCacheKeyForRemoteId(
activity.assetId!,
),
headers: {
"Authorization":
'Bearer ${Store.get(StoreKey.accessToken)}',
},
),
fit: BoxFit.cover,
),
),
child: const SizedBox.shrink(),
)
: null;
}
buildTextField(String? likedId) {
final liked = likedId != null;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: TextField(
controller: inputController,
enabled: !isReadOnly,
focusNode: inputFocusNode,
textInputAction: TextInputAction.send,
autofocus: false,
decoration: InputDecoration(
border: InputBorder.none,
focusedBorder: InputBorder.none,
prefixIcon: currentUser != null
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: UserCircleAvatar(
user: currentUser,
size: 30,
radius: 15,
),
)
: null,
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
icon: Icon(
liked
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
),
onPressed: () async {
liked
? await ref
.read(provider.notifier)
.removeActivity(likedId)
: await ref.read(provider.notifier).addLike();
},
),
),
suffixIconColor: liked ? Colors.red[700] : null,
hintText: isReadOnly
? 'shared_album_activities_input_disable'.tr()
: 'shared_album_activities_input_hint'.tr(),
hintStyle: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
color: Colors.grey[600],
),
),
onEditingComplete: () async {
await ref.read(provider.notifier).addComment(inputController.text);
inputController.clear();
inputFocusNode.unfocus();
listViewScrollController.animateTo(
listViewScrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 800),
curve: Curves.fastOutSlowIn,
);
},
onTapOutside: (_) => inputFocusNode.unfocus(),
),
);
}
getDismissibleWidget(
Widget widget,
Activity activity,
bool canDelete,
) {
return Dismissible(
key: Key(activity.id),
dismissThresholds: const {
DismissDirection.horizontal: 0.7,
},
direction: DismissDirection.horizontal,
confirmDismiss: (direction) => canDelete
? showDialog(
context: context,
builder: (context) => ConfirmDialog(
onOk: () {},
title: "shared_album_activity_remove_title",
content: "shared_album_activity_remove_content",
ok: "delete_dialog_ok",
),
)
: Future.value(false),
onDismissed: (direction) async =>
await ref.read(provider.notifier).removeActivity(activity.id),
background: Container(
color: canDelete ? Colors.red[400] : Colors.grey[600],
alignment: AlignmentDirectional.centerStart,
child: canDelete
? const Padding(
padding: EdgeInsets.all(15),
child: Icon(
Icons.delete_sweep_rounded,
color: Colors.black,
),
)
: null,
),
secondaryBackground: Container(
color: canDelete ? Colors.red[400] : Colors.grey[600],
alignment: AlignmentDirectional.centerEnd,
child: canDelete
? const Padding(
padding: EdgeInsets.all(15),
child: Icon(
Icons.delete_sweep_rounded,
color: Colors.black,
),
)
: null,
),
child: widget,
);
}
return Scaffold(
appBar: AppBar(title: Text(appBarTitle)),
body: activities.maybeWhen(
orElse: () {
return const Center(child: ImmichLoadingIndicator());
},
data: (data) {
final liked = data.firstWhereOrNull(
(a) =>
a.type == ActivityType.like &&
a.user.id == currentUser?.id &&
a.assetId == assetId,
);
return SafeArea(
child: Stack(
children: [
ListView.builder(
controller: listViewScrollController,
itemCount: data.length + 1,
itemBuilder: (context, index) {
// Vertical gap after the last element
if (index == data.length) {
return const SizedBox(
height: 80,
);
}
final activity = data[index];
final canDelete =
activity.user.id == currentUser?.id || isOwner;
return Padding(
padding: const EdgeInsets.all(5),
child: activity.type == ActivityType.comment
? getDismissibleWidget(
ListTile(
minVerticalPadding: 15,
leading: UserCircleAvatar(user: activity.user),
title: buildTitleWithTimestamp(
activity,
leftAlign: withAssetThumbs &&
activity.assetId != null,
),
titleAlignment: ListTileTitleAlignment.top,
trailing: buildAssetThumbnail(activity),
subtitle: Text(activity.comment!),
),
activity,
canDelete,
)
: getDismissibleWidget(
ListTile(
minVerticalPadding: 15,
leading: Container(
width: 44,
alignment: Alignment.center,
child: Icon(
Icons.favorite_rounded,
color: Colors.red[700],
),
),
title: buildTitleWithTimestamp(activity),
trailing: buildAssetThumbnail(activity),
),
activity,
canDelete,
),
);
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
color: context.scaffoldBackgroundColor,
child: buildTextField(liked?.id),
),
),
],
),
);
},
),
);
}
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
@ -10,7 +11,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
class SharedAlbumNotifier extends StateNotifier<List<Album>> { class SharedAlbumNotifier extends StateNotifier<List<Album>> {
SharedAlbumNotifier(this._albumService, Isar db) : super([]) { SharedAlbumNotifier(this._albumService, Isar db, this._ref) : super([]) {
final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc(); final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
query.findAll().then((value) => state = value); query.findAll().then((value) => state = value);
_streamSub = query.watch().listen((data) => state = data); _streamSub = query.watch().listen((data) => state = data);
@ -18,6 +19,7 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
final AlbumService _albumService; final AlbumService _albumService;
late final StreamSubscription<List<Album>> _streamSub; late final StreamSubscription<List<Album>> _streamSub;
final Ref _ref;
Future<Album?> createSharedAlbum( Future<Album?> createSharedAlbum(
String albumName, String albumName,
@ -66,6 +68,17 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
return result; return result;
} }
Future<bool> setActivityEnabled(Album album, bool activityEnabled) async {
final result =
await _albumService.setActivityEnabled(album, activityEnabled);
if (result) {
_ref.invalidate(albumDetailProvider(album.id));
}
return result;
}
@override @override
void dispose() { void dispose() {
_streamSub.cancel(); _streamSub.cancel();
@ -78,5 +91,6 @@ final sharedAlbumProvider =
return SharedAlbumNotifier( return SharedAlbumNotifier(
ref.watch(albumServiceProvider), ref.watch(albumServiceProvider),
ref.watch(dbProvider), ref.watch(dbProvider),
ref,
); );
}); });

View file

@ -284,6 +284,23 @@ class AlbumService {
return false; return false;
} }
Future<bool> setActivityEnabled(Album album, bool enabled) async {
try {
final result = await _apiService.albumApi.updateAlbumInfo(
album.remoteId!,
UpdateAlbumDto(isActivityEnabled: enabled),
);
if (result != null) {
album.activityEnabled = enabled;
await _db.writeTxn(() => _db.albums.put(album));
return true;
}
} catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}");
}
return false;
}
Future<bool> deleteAlbum(Album album) async { Future<bool> deleteAlbum(Album album) async {
try { try {
final userId = Store.get(StoreKey.currentUser).isarId; final userId = Store.get(StoreKey.currentUser).isarId;

View file

@ -1,8 +1,8 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@ -95,20 +95,19 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
children: [ children: [
Text( Text(
'common_add_to_album'.tr(), 'common_add_to_album'.tr(),
style: Theme.of(context).textTheme.displayMedium, style: context.textTheme.displayMedium,
), ),
TextButton.icon( TextButton.icon(
icon: Icon( icon: Icon(
Icons.add, Icons.add,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
label: Text( label: Text(
'common_create_new_album'.tr(), 'common_create_new_album'.tr(),
style: style: TextStyle(color: context.primaryColor),
TextStyle(color: Theme.of(context).primaryColor),
), ),
onPressed: () { onPressed: () {
AutoRouter.of(context).push( context.autoPush(
CreateAlbumRoute( CreateAlbumRoute(
isSharedAlbum: false, isSharedAlbum: false,
initialAssets: assets, initialAssets: assets,

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class AlbumActionOutlinedButton extends StatelessWidget { class AlbumActionOutlinedButton extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
@ -14,8 +15,6 @@ class AlbumActionOutlinedButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon( child: OutlinedButton.icon(
@ -26,7 +25,7 @@ class AlbumActionOutlinedButton extends StatelessWidget {
), ),
side: BorderSide( side: BorderSide(
width: 1, width: 1,
color: isDarkTheme color: context.isDarkTheme
? const Color.fromARGB(255, 63, 63, 63) ? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206), : const Color.fromARGB(255, 206, 206, 206),
), ),
@ -34,11 +33,11 @@ class AlbumActionOutlinedButton extends StatelessWidget {
icon: Icon( icon: Icon(
iconData, iconData,
size: 15, size: 15,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
label: Text( label: Text(
labelText, labelText,
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: context.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),

View file

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
@ -22,7 +23,8 @@ class AlbumThumbnailCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark; var isDarkTheme = context.isDarkTheme;
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
var cardSize = constraints.maxWidth; var cardSize = constraints.maxWidth;
@ -32,7 +34,7 @@ class AlbumThumbnailCard extends StatelessWidget {
height: cardSize, height: cardSize,
width: cardSize, width: cardSize,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], color: isDarkTheme ? Colors.grey[800] : Colors.grey[200],
), ),
child: Center( child: Center(
child: Icon( child: Icon(
@ -73,14 +75,14 @@ class AlbumThumbnailCard extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontFamily: 'WorkSans', fontFamily: 'WorkSans',
fontSize: 12, fontSize: 12,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkTheme ? Colors.white : Colors.black,
), ),
), ),
if (owner != null) const TextSpan(text: ' · '), if (owner != null) const TextSpan(text: ' · '),
if (owner != null) if (owner != null)
TextSpan( TextSpan(
text: owner, text: owner,
style: Theme.of(context).textTheme.labelSmall, style: context.textTheme.labelSmall,
), ),
], ],
), ),
@ -114,8 +116,8 @@ class AlbumThumbnailCard extends StatelessWidget {
album.name, album.name,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDarkMode color: isDarkTheme
? Theme.of(context).primaryColor ? context.primaryColor
: Colors.black, : Colors.black,
), ),
), ),

View file

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
@ -21,12 +21,11 @@ class AlbumThumbnailListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var cardSize = 68.0; var cardSize = 68.0;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
buildEmptyThumbnail() { buildEmptyThumbnail() {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200],
), ),
child: SizedBox( child: SizedBox(
height: cardSize, height: cardSize,
@ -61,7 +60,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: onTap ?? onTap: onTap ??
() { () {
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id)); context.autoPush(AlbumViewerRoute(albumId: album.id));
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 12.0), padding: const EdgeInsets.only(bottom: 12.0),

View file

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
class AlbumTitleTextField extends ConsumerWidget { class AlbumTitleTextField extends ConsumerWidget {
@ -19,7 +20,7 @@ class AlbumTitleTextField extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final isDarkTheme = context.isDarkTheme;
return TextField( return TextField(
onChanged: (v) { onChanged: (v) {
@ -55,7 +56,7 @@ class AlbumTitleTextField extends ConsumerWidget {
}, },
icon: Icon( icon: Icon(
Icons.cancel_rounded, Icons.cancel_rounded,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
splashRadius: 10, splashRadius: 10,
) )

View file

@ -1,8 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
@ -26,6 +27,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
required this.titleFocusNode, required this.titleFocusNode,
this.onAddPhotos, this.onAddPhotos,
this.onAddUsers, this.onAddUsers,
required this.onActivities,
}) : super(key: key); }) : super(key: key);
final Album album; final Album album;
@ -35,11 +37,19 @@ class AlbumViewerAppbar extends HookConsumerWidget
final FocusNode titleFocusNode; final FocusNode titleFocusNode;
final Function(Album album)? onAddPhotos; final Function(Album album)? onAddPhotos;
final Function(Album album)? onAddUsers; final Function(Album album)? onAddUsers;
final Function(Album album) onActivities;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText; final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum; final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
final comments = album.shared
? ref.watch(
activityStatisticsStateProvider(
(albumId: album.remoteId!, assetId: null),
),
)
: 0;
deleteAlbum() async { deleteAlbum() async {
ImmichLoadingOverlayController.appLoader.show(); ImmichLoadingOverlayController.appLoader.show();
@ -48,12 +58,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
if (album.shared) { if (album.shared) {
success = success =
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album); await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
AutoRouter.of(context) context
.navigate(const TabControllerRoute(children: [SharingRoute()])); .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
} else { } else {
success = await ref.watch(albumProvider.notifier).deleteAlbum(album); success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
AutoRouter.of(context) context
.navigate(const TabControllerRoute(children: [LibraryRoute()])); .autoNavigate(const TabControllerRoute(children: [LibraryRoute()]));
} }
if (!success) { if (!success) {
ImmichToast.show( ImmichToast.show(
@ -83,7 +93,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
child: Text( child: Text(
'Cancel', 'Cancel',
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@ -97,9 +107,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
'Confirm', 'Confirm',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).brightness == Brightness.light color: !context.isDarkTheme ? Colors.red : Colors.red[300],
? Colors.red
: Colors.red[300],
), ),
), ),
), ),
@ -120,8 +128,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album); await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
if (isSuccess) { if (isSuccess) {
AutoRouter.of(context) context
.navigate(const TabControllerRoute(children: [SharingRoute()])); .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
} else { } else {
Navigator.pop(context); Navigator.pop(context);
ImmichToast.show( ImmichToast.show(
@ -180,7 +188,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} }
Navigator.of(buildContext).pop(); context.pop();
}, },
); );
return const ShareDialog(); return const ShareDialog();
@ -206,25 +214,29 @@ class AlbumViewerAppbar extends HookConsumerWidget
).tr(), ).tr(),
onTap: () => onShareAssetsTo(), onTap: () => onShareAssetsTo(),
), ),
album.ownerId == userId ? ListTile( album.ownerId == userId
? ListTile(
leading: const Icon(Icons.delete_sweep_rounded), leading: const Icon(Icons.delete_sweep_rounded),
title: const Text( title: const Text(
'album_viewer_appbar_share_remove', 'album_viewer_appbar_share_remove',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
).tr(), ).tr(),
onTap: () => onRemoveFromAlbumPressed(), onTap: () => onRemoveFromAlbumPressed(),
) : const SizedBox(), )
: const SizedBox(),
]; ];
} else { } else {
return [ return [
album.ownerId == userId ? ListTile( album.ownerId == userId
? ListTile(
leading: const Icon(Icons.delete_forever_rounded), leading: const Icon(Icons.delete_forever_rounded),
title: const Text( title: const Text(
'album_viewer_appbar_share_delete', 'album_viewer_appbar_share_delete',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
).tr(), ).tr(),
onTap: () => onDeleteAlbumPressed(), onTap: () => onDeleteAlbumPressed(),
) : ListTile( )
: ListTile(
leading: const Icon(Icons.person_remove_rounded), leading: const Icon(Icons.person_remove_rounded),
title: const Text( title: const Text(
'album_viewer_appbar_share_leave', 'album_viewer_appbar_share_leave',
@ -252,8 +264,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
ListTile( ListTile(
leading: const Icon(Icons.share_rounded), leading: const Icon(Icons.share_rounded),
onTap: () { onTap: () {
AutoRouter.of(context) context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
.push(SharedLinkEditRoute(albumId: album.remoteId));
Navigator.pop(context); Navigator.pop(context);
}, },
title: const Text( title: const Text(
@ -263,8 +274,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
), ),
ListTile( ListTile(
leading: const Icon(Icons.settings_rounded), leading: const Icon(Icons.settings_rounded),
onTap: () => onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)),
AutoRouter.of(context).navigate(AlbumOptionsRoute(album: album)),
title: const Text( title: const Text(
"translated_text_options", "translated_text_options",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
@ -286,7 +296,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
), ),
]; ];
showModalBottomSheet( showModalBottomSheet(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: context.scaffoldBackgroundColor,
isScrollControlled: false, isScrollControlled: false,
context: context, context: context,
builder: (context) { builder: (context) {
@ -310,6 +320,33 @@ class AlbumViewerAppbar extends HookConsumerWidget
); );
} }
Widget buildActivitiesButton() {
return IconButton(
onPressed: () {
onActivities(album);
},
icon: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.mode_comment_outlined,
),
if (comments != 0)
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
comments.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
),
),
],
),
);
}
buildLeadingButton() { buildLeadingButton() {
if (selected.isNotEmpty) { if (selected.isNotEmpty) {
return IconButton( return IconButton(
@ -340,7 +377,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
); );
} else { } else {
return IconButton( return IconButton(
onPressed: () async => await AutoRouter.of(context).pop(), onPressed: () async => await context.autoPop(),
icon: const Icon(Icons.arrow_back_ios_rounded), icon: const Icon(Icons.arrow_back_ios_rounded),
splashRadius: 25, splashRadius: 25,
); );
@ -353,6 +390,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
title: selected.isNotEmpty ? Text('${selected.length}') : null, title: selected.isNotEmpty ? Text('${selected.length}') : null,
centerTitle: false, centerTitle: false,
actions: [ actions: [
if (album.shared && (album.activityEnabled || comments != 0))
buildActivitiesButton(),
if (album.isRemote) if (album.isRemote)
IconButton( IconButton(
splashRadius: 25, splashRadius: 25,

View file

@ -2,6 +2,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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
@ -17,7 +18,6 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final titleTextEditController = useTextEditingController(text: album.name); final titleTextEditController = useTextEditingController(text: album.name);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
void onFocusModeChange() { void onFocusModeChange() {
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@ -65,7 +65,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
}, },
icon: Icon( icon: Icon(
Icons.cancel_rounded, Icons.cancel_rounded,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
splashRadius: 10, splashRadius: 10,
) )
@ -79,14 +79,14 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
focusColor: Colors.grey[300], focusColor: Colors.grey[300],
fillColor: isDarkTheme fillColor: context.isDarkTheme
? const Color.fromARGB(255, 32, 33, 35) ? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200], : Colors.grey[200],
filled: titleFocusNode.hasFocus, filled: titleFocusNode.hasFocus,
hintText: 'share_add_title'.tr(), hintText: 'share_add_title'.tr(),
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 28, fontSize: 28,
color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),

View file

@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -23,6 +23,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
final sharedUsers = useState(album.sharedUsers.toList()); final sharedUsers = useState(album.sharedUsers.toList());
final owner = album.owner.value; final owner = album.owner.value;
final userId = ref.watch(authenticationProvider).userId; final userId = ref.watch(authenticationProvider).userId;
final activityEnabled = useState(album.activityEnabled);
final isOwner = owner?.id == userId; final isOwner = owner?.id == userId;
void showErrorMessage() { void showErrorMessage() {
@ -43,8 +44,9 @@ class AlbumOptionsPage extends HookConsumerWidget {
await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album); await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
if (isSuccess) { if (isSuccess) {
AutoRouter.of(context) context.autoNavigate(
.navigate(const TabControllerRoute(children: [SharingRoute()])); const TabControllerRoute(children: [SharingRoute()]),
);
} else { } else {
showErrorMessage(); showErrorMessage();
} }
@ -96,7 +98,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
} }
showModalBottomSheet( showModalBottomSheet(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: context.scaffoldBackgroundColor,
isScrollControlled: false, isScrollControlled: false,
context: context, context: context,
builder: (context) { builder: (context) {
@ -176,7 +178,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
buildSectionTitle(String text) { buildSectionTitle(String text) {
return Padding( return Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text(text, style: Theme.of(context).textTheme.bodySmall), child: Text(text, style: context.textTheme.bodySmall),
); );
} }
@ -185,7 +187,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded), icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () { onPressed: () {
AutoRouter.of(context).pop(null); context.autoPop(null);
}, },
), ),
centerTitle: true, centerTitle: true,
@ -195,6 +197,29 @@ class AlbumOptionsPage extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (isOwner && album.shared)
SwitchListTile.adaptive(
value: activityEnabled.value,
onChanged: (bool value) async {
activityEnabled.value = value;
if (await ref
.read(sharedAlbumProvider.notifier)
.setActivityEnabled(album, value)) {
album.activityEnabled = value;
}
},
activeColor: activityEnabled.value
? context.primaryColor
: context.themeData.disabledColor,
dense: true,
title: Text(
"shared_album_activity_setting_title",
style: context.textTheme.labelLarge
?.copyWith(fontWeight: FontWeight.bold),
).tr(),
subtitle:
const Text("shared_album_activity_setting_subtitle").tr(),
),
buildSectionTitle("PEOPLE"), buildSectionTitle("PEOPLE"),
buildOwnerInfo(), buildOwnerInfo(),
buildSharedUsersList(), buildSharedUsersList(),

View file

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart';
@ -67,7 +67,7 @@ class AlbumViewerPage extends HookConsumerWidget {
/// If they exist, add to selected asset state to show they are already selected. /// If they exist, add to selected asset state to show they are already selected.
void onAddPhotosPressed(Album albumInfo) async { void onAddPhotosPressed(Album albumInfo) async {
AssetSelectionPageResult? returnPayload = AssetSelectionPageResult? returnPayload =
await AutoRouter.of(context).push<AssetSelectionPageResult?>( await context.autoPush<AssetSelectionPageResult?>(
AssetSelectionRoute( AssetSelectionRoute(
existingAssets: albumInfo.assets, existingAssets: albumInfo.assets,
canDeselect: false, canDeselect: false,
@ -97,8 +97,7 @@ class AlbumViewerPage extends HookConsumerWidget {
} }
void onAddUsersPressed(Album album) async { void onAddUsersPressed(Album album) async {
List<String>? sharedUserIds = List<String>? sharedUserIds = await context.autoPush<List<String>?>(
await AutoRouter.of(context).push<List<String>?>(
SelectAdditionalUserForSharingRoute(album: album), SelectAdditionalUserForSharingRoute(album: album),
); );
@ -171,11 +170,19 @@ class AlbumViewerPage extends HookConsumerWidget {
return const SizedBox(); return const SizedBox();
} }
final String dateRangeText;
if (startDate.day == endDate.day &&
startDate.month == endDate.month &&
startDate.year == endDate.year) {
dateRangeText = DateFormat.yMMMd().format(startDate);
} else {
final String startDateText = (startDate.year == endDate.year final String startDateText = (startDate.year == endDate.year
? DateFormat.MMMd() ? DateFormat.MMMd()
: DateFormat.yMMMd()) : DateFormat.yMMMd())
.format(startDate); .format(startDate);
final String endDateText = DateFormat.yMMMd().format(endDate); final String endDateText = DateFormat.yMMMd().format(endDate);
dateRangeText = "$startDateText - $endDateText";
}
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -183,7 +190,7 @@ class AlbumViewerPage extends HookConsumerWidget {
bottom: album.shared ? 0.0 : 8.0, bottom: album.shared ? 0.0 : 8.0,
), ),
child: Text( child: Text(
"$startDateText - $endDateText", dateRangeText,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -195,7 +202,7 @@ class AlbumViewerPage extends HookConsumerWidget {
Widget buildSharedUserIconsRow(Album album) { Widget buildSharedUserIconsRow(Album album) {
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
await AutoRouter.of(context).push(AlbumOptionsRoute(album: album)); await context.autoPush(AlbumOptionsRoute(album: album));
ref.invalidate(albumDetailProvider(album.id)); ref.invalidate(albumDetailProvider(album.id));
}, },
child: SizedBox( child: SizedBox(
@ -232,6 +239,19 @@ class AlbumViewerPage extends HookConsumerWidget {
); );
} }
onActivitiesPressed(Album album) {
if (album.remoteId != null) {
context.autoPush(
ActivitiesRoute(
albumId: album.remoteId!,
appBarTitle: album.name,
isOwner: userId == album.ownerId,
isReadOnly: !album.activityEnabled,
),
);
}
}
return Scaffold( return Scaffold(
appBar: album.when( appBar: album.when(
data: (data) => AlbumViewerAppbar( data: (data) => AlbumViewerAppbar(
@ -242,6 +262,7 @@ class AlbumViewerPage extends HookConsumerWidget {
selectionDisabled: disableSelection, selectionDisabled: disableSelection,
onAddPhotos: onAddPhotosPressed, onAddPhotos: onAddPhotosPressed,
onAddUsers: onAddUsersPressed, onAddUsers: onAddUsersPressed,
onActivities: onActivitiesPressed,
), ),
error: (error, stackTrace) => AppBar(title: const Text("Error")), error: (error, stackTrace) => AppBar(title: const Text("Error")),
loading: () => AppBar(), loading: () => AppBar(),
@ -266,6 +287,8 @@ class AlbumViewerPage extends HookConsumerWidget {
], ],
), ),
isOwner: userId == data.ownerId, isOwner: userId == data.ownerId,
sharedAlbumId:
data.shared && data.activityEnabled ? data.remoteId : null,
), ),
), ),
), ),

View file

@ -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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
@ -78,7 +79,7 @@ class AssetSelectionPage extends HookConsumerWidget {
canDeselect ? "share_done" : "share_add", canDeselect ? "share_done" : "share_add",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
).tr(), ).tr(),
), ),

View file

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
@ -34,11 +34,11 @@ class CreateAlbumPage extends HookConsumerWidget {
final selectedAssets = useState<Set<Asset>>( final selectedAssets = useState<Set<Asset>>(
initialAssets != null ? Set.from(initialAssets!) : const {}, initialAssets != null ? Set.from(initialAssets!) : const {},
); );
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
showSelectUserPage() async { showSelectUserPage() async {
final bool? ok = await AutoRouter.of(context) final bool? ok = await context.autoPush<bool?>(
.push<bool?>(SelectUserForSharingRoute(assets: selectedAssets.value)); SelectUserForSharingRoute(assets: selectedAssets.value),
);
if (ok == true) { if (ok == true) {
selectedAssets.value = {}; selectedAssets.value = {};
} }
@ -58,7 +58,7 @@ class CreateAlbumPage extends HookConsumerWidget {
onSelectPhotosButtonPressed() async { onSelectPhotosButtonPressed() async {
AssetSelectionPageResult? selectedAsset = AssetSelectionPageResult? selectedAsset =
await AutoRouter.of(context).push<AssetSelectionPageResult?>( await context.autoPush<AssetSelectionPageResult?>(
AssetSelectionRoute( AssetSelectionRoute(
existingAssets: selectedAssets.value, existingAssets: selectedAssets.value,
canDeselect: true, canDeselect: true,
@ -94,7 +94,7 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 200, left: 18), padding: const EdgeInsets.only(top: 200, left: 18),
child: Text( child: Text(
'create_shared_album_page_share_add_assets', 'create_shared_album_page_share_add_assets',
style: Theme.of(context).textTheme.displayMedium?.copyWith( style: context.textTheme.displayMedium?.copyWith(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
@ -117,7 +117,7 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: padding:
const EdgeInsets.symmetric(vertical: 22, horizontal: 16), const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
side: BorderSide( side: BorderSide(
color: isDarkTheme color: context.isDarkTheme
? const Color.fromARGB(255, 63, 63, 63) ? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206), : const Color.fromARGB(255, 206, 206, 206),
), ),
@ -128,13 +128,13 @@ class CreateAlbumPage extends HookConsumerWidget {
onPressed: onSelectPhotosButtonPressed, onPressed: onSelectPhotosButtonPressed,
icon: Icon( icon: Icon(
Icons.add_rounded, Icons.add_rounded,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
label: Padding( label: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
'create_shared_album_page_share_select_photos', 'create_shared_album_page_share_select_photos',
style: Theme.of(context).textTheme.labelLarge?.copyWith( style: context.textTheme.labelLarge?.copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -206,7 +206,7 @@ class CreateAlbumPage extends HookConsumerWidget {
selectedAssets.value = {}; selectedAssets.value = {};
ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id)); context.autoReplace(AlbumViewerRoute(albumId: newAlbum.id));
} }
} }
@ -214,18 +214,18 @@ class CreateAlbumPage extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: context.scaffoldBackgroundColor,
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
selectedAssets.value = {}; selectedAssets.value = {};
AutoRouter.of(context).pop(); context.autoPop();
}, },
icon: const Icon(Icons.close_rounded), icon: const Icon(Icons.close_rounded),
), ),
title: Text( title: Text(
'share_create_album', 'share_create_album',
style: Theme.of(context).textTheme.displayMedium?.copyWith( style: context.textTheme.displayMedium?.copyWith(
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
).tr(), ).tr(),
actions: [ actions: [
@ -239,8 +239,8 @@ class CreateAlbumPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: albumTitleController.text.isEmpty color: albumTitleController.text.isEmpty
? Theme.of(context).disabledColor ? context.themeData.disabledColor
: Theme.of(context).primaryColor, : context.primaryColor,
), ),
), ),
), ),
@ -254,7 +254,7 @@ class CreateAlbumPage extends HookConsumerWidget {
'create_shared_album_page_create'.tr(), 'create_shared_album_page_create'.tr(),
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
), ),
@ -265,7 +265,7 @@ class CreateAlbumPage extends HookConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: context.scaffoldBackgroundColor,
elevation: 5, elevation: 5,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
pinned: true, pinned: true,

View file

@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -21,7 +21,7 @@ class LibraryPage extends HookConsumerWidget {
final trashEnabled = final trashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final albums = ref.watch(albumProvider); final albums = ref.watch(albumProvider);
var isDarkMode = Theme.of(context).brightness == Brightness.dark; var isDarkTheme = context.isDarkTheme;
var settings = ref.watch(appSettingsServiceProvider); var settings = ref.watch(appSettingsServiceProvider);
useEffect( useEffect(
@ -96,15 +96,14 @@ class LibraryPage extends HookConsumerWidget {
padding: const EdgeInsets.only(right: 12.0), padding: const EdgeInsets.only(right: 12.0),
child: Icon( child: Icon(
Icons.check, Icons.check,
color: selected color:
? Theme.of(context).primaryColor selected ? context.primaryColor : Colors.transparent,
: Colors.transparent,
), ),
), ),
Text( Text(
option, option,
style: TextStyle( style: TextStyle(
color: selected ? Theme.of(context).primaryColor : null, color: selected ? context.primaryColor : null,
fontSize: 12.0, fontSize: 12.0,
), ),
), ),
@ -122,13 +121,13 @@ class LibraryPage extends HookConsumerWidget {
Icon( Icon(
Icons.swap_vert_rounded, Icons.swap_vert_rounded,
size: 18, size: 18,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
Text( Text(
options[selectedAlbumSortOrder.value], options[selectedAlbumSortOrder.value],
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontSize: 12.0, fontSize: 12.0,
), ),
), ),
@ -140,7 +139,7 @@ class LibraryPage extends HookConsumerWidget {
Widget buildCreateAlbumButton() { Widget buildCreateAlbumButton() {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false)); context.autoPush(CreateAlbumRoute(isSharedAlbum: false));
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 32), padding: const EdgeInsets.only(bottom: 32),
@ -152,18 +151,18 @@ class LibraryPage extends HookConsumerWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: isDarkMode color: isDarkTheme
? const Color.fromARGB(255, 53, 53, 53) ? const Color.fromARGB(255, 53, 53, 53)
: const Color.fromARGB(255, 203, 203, 203), : const Color.fromARGB(255, 203, 203, 203),
), ),
color: isDarkMode ? Colors.grey[900] : Colors.grey[50], color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Center( child: Center(
child: Icon( child: Icon(
Icons.add_rounded, Icons.add_rounded,
size: 28, size: 28,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
), ),
@ -201,21 +200,21 @@ class LibraryPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 13.0, fontSize: 13.0,
color: isDarkMode ? Colors.white : Colors.grey[800], color: isDarkTheme ? Colors.white : Colors.grey[800],
), ),
), ),
), ),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50], backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
side: BorderSide( side: BorderSide(
color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!, color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!,
), ),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
), ),
icon: Icon( icon: Icon(
icon, icon,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
); );
@ -228,7 +227,7 @@ class LibraryPage extends HookConsumerWidget {
Widget? shareTrashButton() { Widget? shareTrashButton() {
return trashEnabled return trashEnabled
? InkWell( ? InkWell(
onTap: () => AutoRouter.of(context).push(const TrashRoute()), onTap: () => context.autoPush(const TrashRoute()),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: const Icon( child: const Icon(
Icons.delete_rounded, Icons.delete_rounded,
@ -257,12 +256,12 @@ class LibraryPage extends HookConsumerWidget {
children: [ children: [
buildLibraryNavButton( buildLibraryNavButton(
"library_page_favorites".tr(), Icons.favorite_border, () { "library_page_favorites".tr(), Icons.favorite_border, () {
AutoRouter.of(context).navigate(const FavoritesRoute()); context.autoNavigate(const FavoritesRoute());
}), }),
const SizedBox(width: 12.0), const SizedBox(width: 12.0),
buildLibraryNavButton( buildLibraryNavButton(
"library_page_archive".tr(), Icons.archive_outlined, () { "library_page_archive".tr(), Icons.archive_outlined, () {
AutoRouter.of(context).navigate(const ArchiveRoute()); context.autoNavigate(const ArchiveRoute());
}), }),
], ],
), ),
@ -306,7 +305,7 @@ class LibraryPage extends HookConsumerWidget {
return AlbumThumbnailCard( return AlbumThumbnailCard(
album: sorted[index - 1], album: sorted[index - 1],
onTap: () => AutoRouter.of(context).push( onTap: () => context.autoPush(
AlbumViewerRoute( AlbumViewerRoute(
albumId: sorted[index - 1].id, albumId: sorted[index - 1].id,
), ),
@ -348,7 +347,7 @@ class LibraryPage extends HookConsumerWidget {
childCount: local.length, childCount: local.length,
(context, index) => AlbumThumbnailCard( (context, index) => AlbumThumbnailCard(
album: local[index], album: local[index],
onTap: () => AutoRouter.of(context).push( onTap: () => context.autoPush(
AlbumViewerRoute( AlbumViewerRoute(
albumId: local[index].id, albumId: local[index].id,
), ),

View file

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
@ -22,14 +22,13 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
final sharedUsersList = useState<Set<User>>({}); final sharedUsersList = useState<Set<User>>({});
addNewUsersHandler() { addNewUsersHandler() {
AutoRouter.of(context) context.autoPop(sharedUsersList.value.map((e) => e.id).toList());
.pop(sharedUsersList.value.map((e) => e.id).toList());
} }
buildTileIcon(User user) { buildTileIcon(User user) {
if (sharedUsersList.value.contains(user)) { if (sharedUsersList.value.contains(user)) {
return CircleAvatar( return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: context.primaryColor,
child: const Icon( child: const Icon(
Icons.check_rounded, Icons.check_rounded,
size: 25, size: 25,
@ -50,7 +49,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip( child: Chip(
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15), backgroundColor: context.primaryColor.withOpacity(0.15),
label: Text( label: Text(
user.email, user.email,
style: const TextStyle( style: const TextStyle(
@ -124,7 +123,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close_rounded), icon: const Icon(Icons.close_rounded),
onPressed: () { onPressed: () {
AutoRouter.of(context).pop(null); context.autoPop(null);
}, },
), ),
actions: [ actions: [

View file

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
@ -35,9 +35,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
// ref.watch(assetSelectionProvider.notifier).removeAll(); // ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
AutoRouter.of(context).pop(true); context.autoPop(true);
AutoRouter.of(context) context
.navigate(const TabControllerRoute(children: [SharingRoute()])); .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
} }
ScaffoldMessenger( ScaffoldMessenger(
@ -50,7 +50,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
buildTileIcon(User user) { buildTileIcon(User user) {
if (sharedUsersList.value.contains(user)) { if (sharedUsersList.value.contains(user)) {
return CircleAvatar( return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: context.primaryColor,
child: const Icon( child: const Icon(
Icons.check_rounded, Icons.check_rounded,
size: 25, size: 25,
@ -71,7 +71,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip( child: Chip(
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15), backgroundColor: context.primaryColor.withOpacity(0.15),
label: Text( label: Text(
user.email, user.email,
style: const TextStyle( style: const TextStyle(
@ -139,20 +139,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
'share_invite', 'share_invite',
style: TextStyle(color: Theme.of(context).primaryColor), style: TextStyle(color: context.primaryColor),
).tr(), ).tr(),
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close_rounded), icon: const Icon(Icons.close_rounded),
onPressed: () async { onPressed: () async {
AutoRouter.of(context).pop(); context.autoPop();
}, },
), ),
actions: [ actions: [
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor, foregroundColor: context.primaryColor,
), ),
onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
child: const Text( child: const Text(
@ -160,7 +160,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
// color: Theme.of(context).primaryColor, // color: context.primaryColor,
), ),
).tr(), ).tr(),
), ),

View file

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
@ -21,7 +21,6 @@ class SharingPage extends HookConsumerWidget {
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider); final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
final userId = ref.watch(currentUserProvider)?.id; final userId = ref.watch(currentUserProvider)?.id;
final partner = ref.watch(partnerSharedWithProvider); final partner = ref.watch(partnerSharedWithProvider);
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect( useEffect(
() { () {
@ -47,8 +46,9 @@ class SharingPage extends HookConsumerWidget {
album: sharedAlbums[index], album: sharedAlbums[index],
showOwner: true, showOwner: true,
onTap: () { onTap: () {
AutoRouter.of(context) context.autoPush(
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id)); AlbumViewerRoute(albumId: sharedAlbums[index].id),
);
}, },
); );
}, },
@ -79,11 +79,10 @@ class SharingPage extends HookConsumerWidget {
album.name, album.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDarkMode color:
? Theme.of(context).primaryColor context.isDarkTheme ? context.primaryColor : Colors.black,
: Colors.black,
), ),
), ),
subtitle: isOwner subtitle: isOwner
@ -103,8 +102,9 @@ class SharingPage extends HookConsumerWidget {
) )
: null, : null,
onTap: () { onTap: () {
AutoRouter.of(context) context.autoPush(
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id)); AlbumViewerRoute(albumId: sharedAlbums[index].id),
);
}, },
); );
}, },
@ -127,8 +127,7 @@ class SharingPage extends HookConsumerWidget {
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
AutoRouter.of(context) context.autoPush(CreateAlbumRoute(isSharedAlbum: true));
.push(CreateAlbumRoute(isSharedAlbum: true));
}, },
icon: const Icon( icon: const Icon(
Icons.photo_album_outlined, Icons.photo_album_outlined,
@ -147,8 +146,7 @@ class SharingPage extends HookConsumerWidget {
const SizedBox(width: 12.0), const SizedBox(width: 12.0),
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => onPressed: () => context.autoPush(const SharedLinkRoute()),
AutoRouter.of(context).push(const SharedLinkRoute()),
icon: const Icon( icon: const Icon(
Icons.link, Icons.link,
size: 20, size: 20,
@ -191,21 +189,21 @@ class SharingPage extends HookConsumerWidget {
child: Icon( child: Icon(
Icons.insert_photo_rounded, Icons.insert_photo_rounded,
size: 50, size: 50,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'sharing_page_empty_list', 'sharing_page_empty_list',
style: Theme.of(context).textTheme.displaySmall, style: context.textTheme.displaySmall,
).tr(), ).tr(),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'sharing_page_description', 'sharing_page_description',
style: Theme.of(context).textTheme.bodyMedium, style: context.textTheme.bodyMedium,
).tr(), ).tr(),
), ),
], ],
@ -218,7 +216,7 @@ class SharingPage extends HookConsumerWidget {
Widget sharePartnerButton() { Widget sharePartnerButton() {
return InkWell( return InkWell(
onTap: () => AutoRouter.of(context).push(const PartnerRoute()), onTap: () => context.autoPush(const PartnerRoute()),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: const Icon( child: const Icon(
Icons.swap_horizontal_circle_rounded, Icons.swap_horizontal_circle_rounded,

View file

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart'; import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
@ -30,7 +30,7 @@ class ArchivePage extends HookConsumerWidget {
AppBar buildAppBar(String count) { AppBar buildAppBar(String count) {
return AppBar( return AppBar(
leading: IconButton( leading: IconButton(
onPressed: () => AutoRouter.of(context).pop(), onPressed: () => context.autoPop(),
icon: const Icon(Icons.arrow_back_ios_rounded), icon: const Icon(Icons.arrow_back_ios_rounded),
), ),
centerTitle: true, centerTitle: true,

View file

@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart'; import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
@ -67,7 +68,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
} }
Navigator.of(buildContext).pop(); context.pop();
}, },
); );
return const ShareDialog(); return const ShareDialog();

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
class AdvancedBottomSheet extends HookConsumerWidget { class AdvancedBottomSheet extends HookConsumerWidget {
@ -11,8 +12,6 @@ class AdvancedBottomSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return SingleChildScrollView( return SingleChildScrollView(
child: Card( child: Card(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@ -40,7 +39,9 @@ class AdvancedBottomSheet extends HookConsumerWidget {
const SizedBox(height: 32.0), const SizedBox(height: 32.0),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[200], color: context.isDarkTheme
? Colors.grey[900]
: Colors.grey[200],
borderRadius: BorderRadius.circular(15.0), borderRadius: BorderRadius.circular(15.0),
), ),
child: Padding( child: Padding(
@ -70,7 +71,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
icon: Icon( icon: Icon(
Icons.copy, Icons.copy,
size: 16.0, size: 16.0,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
), ),

View file

@ -2,6 +2,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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart';
@ -19,8 +20,7 @@ class DescriptionInput extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final textColor = context.isDarkTheme ? Colors.white : Colors.black;
final textColor = isDarkTheme ? Colors.white : Colors.black;
final controller = useTextEditingController(); final controller = useTextEditingController();
final focusNode = useFocusNode(); final focusNode = useFocusNode();
final isFocus = useState(false); final isFocus = useState(false);

View file

@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:timezone/timezone.dart'; import 'package:timezone/timezone.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart'; import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
@ -41,10 +42,16 @@ class ExifBottomSheet extends HookConsumerWidget {
final location = getLocation(asset.exifInfo!.timeZone!); final location = getLocation(asset.exifInfo!.timeZone!);
dt = TZDateTime.from(dt, location); dt = TZDateTime.from(dt, location);
} on LocationNotFoundException { } on LocationNotFoundException {
RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); RegExp re = RegExp(
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
caseSensitive: false,
);
final m = re.firstMatch(asset.exifInfo!.timeZone!); final m = re.firstMatch(asset.exifInfo!.timeZone!);
if (m != null) { if (m != null) {
final duration = Duration(hours: int.parse(m.group(1) ?? '0'), minutes: int.parse(m.group(2) ?? '0')); final duration = Duration(
hours: int.parse(m.group(1) ?? '0'),
minutes: int.parse(m.group(2) ?? '0'),
);
dt = dt.add(duration); dt = dt.add(duration);
timeZone = formatTimeZone(duration); timeZone = formatTimeZone(duration);
} }
@ -105,8 +112,7 @@ class ExifBottomSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final assetWithExif = ref.watch(assetDetailProvider(asset)); final assetWithExif = ref.watch(assetDetailProvider(asset));
final exifInfo = (assetWithExif.value ?? asset).exifInfo; final exifInfo = (assetWithExif.value ?? asset).exifInfo;
var isDarkTheme = Theme.of(context).brightness == Brightness.dark; var textColor = context.isDarkTheme ? Colors.white : Colors.black;
var textColor = isDarkTheme ? Colors.white : Colors.black;
buildMap() { buildMap() {
return Padding( return Padding(
@ -322,9 +328,14 @@ class ExifBottomSheet extends HookConsumerWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null ? Text( subtitle: exifInfo.f != null ||
exifInfo.exposureSeconds != null ||
exifInfo.mm != null ||
exifInfo.iso != null
? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
) : null, )
: null,
), ),
], ],
); );
@ -393,7 +404,7 @@ class ExifBottomSheet extends HookConsumerWidget {
data: (data) => DescriptionInput(asset: data), data: (data) => DescriptionInput(asset: data),
error: (error, stackTrace) => Icon( error: (error, stackTrace) => Icon(
Icons.image_not_supported_outlined, Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
loading: () => const SizedBox( loading: () => const SizedBox(
width: 75, width: 75,

View file

@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
@ -16,6 +17,8 @@ class TopControlAppBar extends HookConsumerWidget {
required this.onFavorite, required this.onFavorite,
required this.onUploadPressed, required this.onUploadPressed,
required this.isOwner, required this.isOwner,
required this.shareAlbumId,
required this.onActivitiesPressed,
}) : super(key: key); }) : super(key: key);
final Asset asset; final Asset asset;
@ -24,14 +27,23 @@ class TopControlAppBar extends HookConsumerWidget {
final VoidCallback? onDownloadPressed; final VoidCallback? onDownloadPressed;
final VoidCallback onToggleMotionVideo; final VoidCallback onToggleMotionVideo;
final VoidCallback onAddToAlbumPressed; final VoidCallback onAddToAlbumPressed;
final VoidCallback onActivitiesPressed;
final Function(Asset) onFavorite; final Function(Asset) onFavorite;
final bool isPlayingMotionVideo; final bool isPlayingMotionVideo;
final bool isOwner; final bool isOwner;
final String? shareAlbumId;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
const double iconSize = 22.0; const double iconSize = 22.0;
final a = ref.watch(assetWatcher(asset)).value ?? asset; final a = ref.watch(assetWatcher(asset)).value ?? asset;
final comments = shareAlbumId != null
? ref.watch(
activityStatisticsStateProvider(
(albumId: shareAlbumId!, assetId: asset.remoteId),
),
)
: 0;
Widget buildFavoriteButton(a) { Widget buildFavoriteButton(a) {
return IconButton( return IconButton(
@ -94,6 +106,34 @@ class TopControlAppBar extends HookConsumerWidget {
); );
} }
Widget buildActivitiesButton() {
return IconButton(
onPressed: () {
onActivitiesPressed();
},
icon: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.mode_comment_outlined,
color: Colors.grey[200],
),
if (comments != 0)
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
comments.toString(),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey[200],
),
),
),
],
),
);
}
Widget buildUploadButton() { Widget buildUploadButton() {
return IconButton( return IconButton(
onPressed: onUploadPressed, onPressed: onUploadPressed,
@ -107,7 +147,7 @@ class TopControlAppBar extends HookConsumerWidget {
Widget buildBackButton() { Widget buildBackButton() {
return IconButton( return IconButton(
onPressed: () { onPressed: () {
AutoRouter.of(context).pop(); context.autoPop();
}, },
icon: Icon( icon: Icon(
Icons.arrow_back_ios_new_rounded, Icons.arrow_back_ios_new_rounded,
@ -130,6 +170,7 @@ class TopControlAppBar extends HookConsumerWidget {
if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
if (asset.isRemote && isOwner) buildAddToAlbumButtom(), if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
if (shareAlbumId != null) buildActivitiesButton(),
buildMoreInfoButton(), buildMoreInfoButton(),
], ],
); );

View file

@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
@ -49,6 +50,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final int heroOffset; final int heroOffset;
final bool showStack; final bool showStack;
final bool isOwner; final bool isOwner;
final String? sharedAlbumId;
GalleryViewerPage({ GalleryViewerPage({
super.key, super.key,
@ -58,6 +60,7 @@ class GalleryViewerPage extends HookConsumerWidget {
this.heroOffset = 0, this.heroOffset = 0,
this.showStack = false, this.showStack = false,
this.isOwner = true, this.isOwner = true,
this.sharedAlbumId,
}) : controller = PageController(initialPage: initialIndex); }) : controller = PageController(initialPage: initialIndex);
final PageController controller; final PageController controller;
@ -207,7 +210,7 @@ class GalleryViewerPage extends HookConsumerWidget {
if (isDeleted && isParent) { if (isDeleted && isParent) {
if (totalAssets == 1) { if (totalAssets == 1) {
// Handle only one asset // Handle only one asset
AutoRouter.of(context).pop(); context.autoPop();
} else { } else {
// Go to next page otherwise // Go to next page otherwise
controller.nextPage( controller.nextPage(
@ -291,7 +294,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final ratio = d.dy / max(d.dx.abs(), 1); final ratio = d.dy / max(d.dx.abs(), 1);
if (d.dy > sensitivity && ratio > ratioThreshold) { if (d.dy > sensitivity && ratio > ratioThreshold) {
AutoRouter.of(context).pop(); context.autoPop();
} else if (d.dy < -sensitivity && ratio < -ratioThreshold) { } else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
showInfo(); showInfo();
} }
@ -306,7 +309,7 @@ class GalleryViewerPage extends HookConsumerWidget {
.watch(assetProvider.notifier) .watch(assetProvider.notifier)
.toggleArchive([asset], !asset.isArchived); .toggleArchive([asset], !asset.isArchived);
if (isParent) { if (isParent) {
AutoRouter.of(context).pop(); context.autoPop();
return; return;
} }
removeAssetFromStack(); removeAssetFromStack();
@ -327,6 +330,19 @@ class GalleryViewerPage extends HookConsumerWidget {
); );
} }
handleActivities() {
if (sharedAlbumId != null) {
context.autoPush(
ActivitiesRoute(
albumId: sharedAlbumId!,
assetId: asset().remoteId,
withAssetThumbs: false,
isOwner: isOwner,
),
);
}
}
buildAppBar() { buildAppBar() {
return IgnorePointer( return IgnorePointer(
ignoring: !ref.watch(showControlsProvider), ignoring: !ref.watch(showControlsProvider),
@ -355,6 +371,8 @@ class GalleryViewerPage extends HookConsumerWidget {
isPlayingMotionVideo.value = !isPlayingMotionVideo.value; isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
}), }),
onAddToAlbumPressed: () => addToAlbum(asset()), onAddToAlbumPressed: () => addToAlbum(asset()),
shareAlbumId: sharedAlbumId,
onActivitiesPressed: handleActivities,
), ),
), ),
), ),
@ -497,7 +515,7 @@ class GalleryViewerPage extends HookConsumerWidget {
stackElements.elementAt(stackIndex.value), stackElements.elementAt(stackIndex.value),
); );
Navigator.pop(ctx); Navigator.pop(ctx);
AutoRouter.of(context).pop(); context.autoPop();
}, },
title: const Text( title: const Text(
"viewer_stack_use_as_main_asset", "viewer_stack_use_as_main_asset",
@ -524,7 +542,7 @@ class GalleryViewerPage extends HookConsumerWidget {
childrenToRemove: [currentAsset], childrenToRemove: [currentAsset],
); );
Navigator.pop(ctx); Navigator.pop(ctx);
AutoRouter.of(context).pop(); context.autoPop();
} else { } else {
await ref.read(assetStackServiceProvider).updateStack( await ref.read(assetStackServiceProvider).updateStack(
currentAsset, currentAsset,
@ -552,7 +570,7 @@ class GalleryViewerPage extends HookConsumerWidget {
childrenToRemove: stack, childrenToRemove: stack,
); );
Navigator.pop(ctx); Navigator.pop(ctx);
AutoRouter.of(context).pop(); context.autoPop();
}, },
title: const Text( title: const Text(
"viewer_unstack", "viewer_unstack",
@ -812,8 +830,8 @@ class GalleryViewerPage extends HookConsumerWidget {
placeholder: Image( placeholder: Image(
image: provider, image: provider,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
height: MediaQuery.of(context).size.height, height: context.height,
width: MediaQuery.of(context).size.width, width: context.width,
alignment: Alignment.center, alignment: Alignment.center,
), ),
onVideoEnded: () { onVideoEnded: () {

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
@ -44,7 +45,7 @@ class VideoViewerPage extends HookConsumerWidget {
), ),
error: (error, stackTrace) => Icon( error: (error, stackTrace) => Icon(
Icons.image_not_supported_outlined, Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
loading: () => const Center( loading: () => const Center(
child: SizedBox( child: SizedBox(
@ -74,8 +75,8 @@ class VideoViewerPage extends HookConsumerWidget {
), ),
if (downloadAssetStatus == DownloadAssetStatus.loading) if (downloadAssetStatus == DownloadAssetStatus.loading)
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height, height: context.height,
width: MediaQuery.of(context).size.width, width: context.width,
child: const Center( child: const Center(
child: ImmichLoadingIndicator(), child: ImmichLoadingIndicator(),
), ),
@ -205,8 +206,8 @@ class _VideoPlayerState extends State<VideoPlayer> {
); );
} else { } else {
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height, height: context.height,
width: MediaQuery.of(context).size.width, width: context.width,
child: Center( child: Center(
child: Stack( child: Stack(
children: [ children: [

View file

@ -40,7 +40,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInPercentage: 0, progressInPercentage: 0,
cancelToken: CancellationToken(), cancelToken: CancellationToken(),
autoBackup: Store.get(StoreKey.autoBackup, false), autoBackup: Store.get(StoreKey.autoBackup, false),
backgroundBackup: false, backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true), backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
backupRequireCharging: backupRequireCharging:
Store.get(StoreKey.backupRequireCharging, false), Store.get(StoreKey.backupRequireCharging, false),
@ -171,6 +171,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state.backupRequireCharging, state.backupRequireCharging,
); );
await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay); await Store.put(StoreKey.backupTriggerDelay, state.backupTriggerDelay);
await Store.put(StoreKey.backgroundBackup, state.backgroundBackup);
} else { } else {
state = state.copyWith( state = state.copyWith(
backgroundBackup: wasEnabled, backgroundBackup: wasEnabled,
@ -383,6 +384,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final isEnabled = await _backgroundService.isBackgroundBackupEnabled(); final isEnabled = await _backgroundService.isBackgroundBackupEnabled();
state = state.copyWith(backgroundBackup: isEnabled); state = state.copyWith(backgroundBackup: isEnabled);
if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) {
Store.put(StoreKey.backgroundBackup, isEnabled);
}
if (state.backupProgress != BackUpProgressEnum.inBackground) { if (state.backupProgress != BackUpProgressEnum.inBackground) {
await _getBackupAlbumsInfo(); await _getBackupAlbumsInfo();

View file

@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -22,10 +22,10 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded = final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final isDarkTheme = context.isDarkTheme;
ColorFilter selectedFilter = ColorFilter.mode( ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100), context.primaryColor.withAlpha(100),
BlendMode.darken, BlendMode.darken,
); );
ColorFilter excludedFilter = ColorFilter excludedFilter =
@ -46,7 +46,7 @@ class AlbumInfoCard extends HookConsumerWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),
backgroundColor: Theme.of(context).primaryColor, backgroundColor: context.primaryColor,
); );
} else if (isExcluded) { } else if (isExcluded) {
return Chip( return Chip(
@ -194,7 +194,7 @@ class AlbumInfoCard extends HookConsumerWidget {
albumInfo.name, albumInfo.name,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@ -224,13 +224,13 @@ class AlbumInfoCard extends HookConsumerWidget {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
AutoRouter.of(context).push( context.autoPush(
AlbumPreviewRoute(album: albumInfo.albumEntity), AlbumPreviewRoute(album: albumInfo.albumEntity),
); );
}, },
icon: Icon( icon: Icon(
Icons.image_outlined, Icons.image_outlined,
color: Theme.of(context).primaryColor, color: context.primaryColor,
size: 24, size: 24,
), ),
splashRadius: 25, splashRadius: 25,

View file

@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -25,14 +25,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode( ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100), context.primaryColor.withAlpha(100),
BlendMode.darken, BlendMode.darken,
); );
ColorFilter excludedFilter = ColorFilter excludedFilter =
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter = ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color); const ColorFilter.mode(Colors.black, BlendMode.color);
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
var assetCount = useState(0); var assetCount = useState(0);
@ -56,11 +55,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
buildTileColor() { buildTileColor() {
if (isSelected) { if (isSelected) {
return isDarkTheme return context.isDarkTheme
? Theme.of(context).primaryColor.withAlpha(100) ? context.primaryColor.withAlpha(100)
: Theme.of(context).primaryColor.withAlpha(25); : context.primaryColor.withAlpha(25);
} else if (isExcluded) { } else if (isExcluded) {
return isDarkTheme return context.isDarkTheme
? Colors.red[300]?.withAlpha(150) ? Colors.red[300]?.withAlpha(150)
: Colors.red[100]?.withAlpha(150); : Colors.red[100]?.withAlpha(150);
} else { } else {
@ -159,13 +158,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
subtitle: Text(assetCount.value.toString()), subtitle: Text(assetCount.value.toString()),
trailing: IconButton( trailing: IconButton(
onPressed: () { onPressed: () {
AutoRouter.of(context).push( context.autoPush(
AlbumPreviewRoute(album: albumInfo.albumEntity), AlbumPreviewRoute(album: albumInfo.albumEntity),
); );
}, },
icon: Icon( icon: Icon(
Icons.image_outlined, Icons.image_outlined,
color: Theme.of(context).primaryColor, color: context.primaryColor,
size: 24, size: 24,
), ),
splashRadius: 25, splashRadius: 25,

View file

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class BackupInfoCard extends StatelessWidget { class BackupInfoCard extends StatelessWidget {
final String title; final String title;
@ -14,13 +15,11 @@ class BackupInfoCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Card( return Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // if you need this borderRadius: BorderRadius.circular(20), // if you need this
side: BorderSide( side: BorderSide(
color: isDarkMode color: context.isDarkTheme
? const Color.fromARGB(255, 56, 56, 56) ? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12, : Colors.black12,
width: 1, width: 1,

View file

@ -1,9 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
@ -53,7 +53,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
), ),
backgroundColor: Colors.white, backgroundColor: Colors.white,
onPressed: () { onPressed: () {
AutoRouter.of(context).push(const FailedBackupStatusRoute()); context.autoPush(const FailedBackupStatusRoute());
}, },
); );
} }
@ -61,7 +61,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
Widget buildAssetInfoTable() { Widget buildAssetInfoTable() {
return Table( return Table(
border: TableBorder.all( border: TableBorder.all(
color: Theme.of(context).primaryColorLight, color: context.themeData.primaryColorLight,
width: 1, width: 1,
), ),
children: [ children: [
@ -176,7 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
onTap: () => isShowThumbnail.value = true, onTap: () => isShowThumbnail.value = true,
child: Icon( child: Icon(
Icons.image_outlined, Icons.image_outlined,
color: Theme.of(context).primaryColor, color: context.primaryColor,
size: 30, size: 30,
), ),
), ),
@ -206,7 +206,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
minHeight: 10.0, minHeight: 10.0,
value: uploadProgress / 100.0, value: uploadProgress / 100.0,
backgroundColor: Colors.grey, backgroundColor: Colors.grey,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
Text( Text(

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -43,7 +44,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
subtitle: Text( subtitle: Text(
@ -54,7 +55,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
), ),
leading: Icon( leading: Icon(
Icons.bug_report, Icons.bug_report,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
); );
} }

View file

@ -1,9 +1,9 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:auto_route/auto_route.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -53,7 +53,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
], ],
), ),
leading: IconButton( leading: IconButton(
onPressed: () => AutoRouter.of(context).pop(), onPressed: () => context.autoPop(),
icon: const Icon(Icons.arrow_back_ios_new_rounded), icon: const Icon(Icons.arrow_back_ios_new_rounded),
), ),
), ),

View file

@ -1,10 +1,10 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
@ -18,7 +18,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
// final availableAlbums = ref.watch(backupProvider).availableAlbums; // final availableAlbums = ref.watch(backupProvider).availableAlbums;
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final isDarkTheme = context.isDarkTheme;
final allAlbums = ref.watch(backupProvider).availableAlbums; final allAlbums = ref.watch(backupProvider).availableAlbums;
// Albums which are displayed to the user // Albums which are displayed to the user
@ -118,7 +118,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
backgroundColor: Theme.of(context).primaryColor, backgroundColor: context.primaryColor,
deleteIconColor: isDarkTheme ? Colors.black : Colors.white, deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
deleteIcon: const Icon( deleteIcon: const Icon(
Icons.cancel_rounded, Icons.cancel_rounded,
@ -211,7 +211,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
onPressed: () => AutoRouter.of(context).pop(), onPressed: () => context.autoPop(),
icon: const Icon(Icons.arrow_back_ios_rounded), icon: const Icon(Icons.arrow_back_ios_rounded),
), ),
title: const Text( title: const Text(
@ -315,7 +315,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
"backup_album_selection_page_albums_tap", "backup_album_selection_page_albums_tap",
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),
@ -325,7 +325,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
icon: Icon( icon: Icon(
Icons.info, Icons.info,
size: 20, size: 20,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
onPressed: () { onPressed: () {
// show the dialog // show the dialog
@ -342,7 +342,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
).tr(), ).tr(),
content: SingleChildScrollView( content: SingleChildScrollView(

View file

@ -1,11 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
@ -49,7 +49,6 @@ class BackupControllerPage extends HookConsumerWidget {
!hasExclusiveAccess !hasExclusiveAccess
? false ? false
: true; : true;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
final checkInProgress = useState(false); final checkInProgress = useState(false);
useEffect( useEffect(
@ -151,7 +150,7 @@ class BackupControllerPage extends HookConsumerWidget {
return ListTile( return ListTile(
leading: Icon( leading: Icon(
Icons.warning_rounded, Icons.warning_rounded,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
title: const Text( title: const Text(
"Check for corrupt asset backups", "Check for corrupt asset backups",
@ -187,7 +186,7 @@ class BackupControllerPage extends HookConsumerWidget {
leading: isAutoBackup leading: isAutoBackup
? Icon( ? Icon(
Icons.cloud_done_rounded, Icons.cloud_done_rounded,
color: Theme.of(context).primaryColor, color: context.primaryColor,
) )
: const Icon(Icons.cloud_off_rounded), : const Icon(Icons.cloud_off_rounded),
title: Text( title: Text(
@ -266,7 +265,7 @@ class BackupControllerPage extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
).tr(), ).tr(),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); context.pop();
}, },
), ),
], ],
@ -279,7 +278,7 @@ class BackupControllerPage extends HookConsumerWidget {
final bool isBackgroundEnabled = backupState.backgroundBackup; final bool isBackgroundEnabled = backupState.backgroundBackup;
final bool isWifiRequired = backupState.backupRequireWifi; final bool isWifiRequired = backupState.backupRequireWifi;
final bool isChargingRequired = backupState.backupRequireCharging; final bool isChargingRequired = backupState.backupRequireCharging;
final Color activeColor = Theme.of(context).primaryColor; final Color activeColor = context.primaryColor;
String formatBackupDelaySliderValue(double v) { String formatBackupDelaySliderValue(double v) {
if (v == 0.0) { if (v == 0.0) {
@ -410,7 +409,7 @@ class BackupControllerPage extends HookConsumerWidget {
max: 3.0, max: 3.0,
divisions: 3, divisions: 3,
label: formatBackupDelaySliderValue(triggerDelay.value), label: formatBackupDelaySliderValue(triggerDelay.value),
activeColor: Theme.of(context).primaryColor, activeColor: context.primaryColor,
), ),
), ),
ElevatedButton( ElevatedButton(
@ -511,7 +510,7 @@ class BackupControllerPage extends HookConsumerWidget {
child: Text( child: Text(
text.trim().substring(0, text.length - 2), text.trim().substring(0, text.length - 2),
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -523,7 +522,7 @@ class BackupControllerPage extends HookConsumerWidget {
child: Text( child: Text(
"backup_controller_page_none_selected".tr(), "backup_controller_page_none_selected".tr(),
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@ -562,7 +561,7 @@ class BackupControllerPage extends HookConsumerWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
side: BorderSide( side: BorderSide(
color: isDarkMode color: context.isDarkTheme
? const Color.fromARGB(255, 56, 56, 56) ? const Color.fromARGB(255, 56, 56, 56)
: Colors.black12, : Colors.black12,
width: 1, width: 1,
@ -592,7 +591,7 @@ class BackupControllerPage extends HookConsumerWidget {
), ),
trailing: ElevatedButton( trailing: ElevatedButton(
onPressed: () { onPressed: () {
AutoRouter.of(context).push(const BackupAlbumSelectionRoute()); context.autoPush(const BackupAlbumSelectionRoute());
}, },
child: const Text( child: const Text(
"backup_controller_page_select", "backup_controller_page_select",
@ -678,7 +677,7 @@ class BackupControllerPage extends HookConsumerWidget {
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
ref.watch(websocketProvider.notifier).listenUploadEvent(); ref.watch(websocketProvider.notifier).listenUploadEvent();
AutoRouter.of(context).pop(true); context.autoPop(true);
}, },
splashRadius: 24, splashRadius: 24,
icon: const Icon( icon: const Icon(

View file

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -20,7 +20,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
), ),
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
AutoRouter.of(context).pop(true); context.autoPop(true);
}, },
splashRadius: 24, splashRadius: 24,
icon: const Icon( icon: const Icon(
@ -114,7 +114,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 12,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
), ),

View file

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; 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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
@ -28,7 +28,7 @@ class FavoritesPage extends HookConsumerWidget {
AppBar buildAppBar() { AppBar buildAppBar() {
return AppBar( return AppBar(
leading: IconButton( leading: IconButton(
onPressed: () => AutoRouter.of(context).pop(), onPressed: () => context.autoPop(),
icon: const Icon(Icons.arrow_back_ios_rounded), icon: const Icon(Icons.arrow_back_ios_rounded),
), ),
centerTitle: true, centerTitle: true,

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class GroupDividerTitle extends ConsumerWidget { class GroupDividerTitle extends ConsumerWidget {
const GroupDividerTitle({ const GroupDividerTitle({
@ -51,7 +52,7 @@ class GroupDividerTitle extends ConsumerWidget {
child: multiselectEnabled && selected child: multiselectEnabled && selected
? Icon( ? Icon(
Icons.check_circle_rounded, Icons.check_circle_rounded,
color: Theme.of(context).primaryColor, color: context.primaryColor,
) )
: const Icon( : const Icon(
Icons.check_circle_outline_rounded, Icons.check_circle_outline_rounded,

View file

@ -34,6 +34,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
final bool showDragScroll; final bool showDragScroll;
final bool showStack; final bool showStack;
final bool isOwner; final bool isOwner;
final String? sharedAlbumId;
const ImmichAssetGrid({ const ImmichAssetGrid({
super.key, super.key,
@ -55,6 +56,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
this.showDragScroll = true, this.showDragScroll = true,
this.showStack = false, this.showStack = false,
this.isOwner = true, this.isOwner = true,
this.sharedAlbumId,
}); });
@override @override
@ -120,6 +122,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
showDragScroll: showDragScroll, showDragScroll: showDragScroll,
showStack: showStack, showStack: showStack,
isOwner: isOwner, isOwner: isOwner,
sharedAlbumId: sharedAlbumId,
), ),
); );
} }

View file

@ -4,10 +4,11 @@ import 'dart:math';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/utils/builtin_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart'; import 'asset_grid_data_structure.dart';
import 'group_divider_title.dart'; import 'group_divider_title.dart';
@ -39,6 +40,7 @@ class ImmichAssetGridView extends StatefulWidget {
final bool showDragScroll; final bool showDragScroll;
final bool showStack; final bool showStack;
final bool isOwner; final bool isOwner;
final String? sharedAlbumId;
const ImmichAssetGridView({ const ImmichAssetGridView({
super.key, super.key,
@ -60,6 +62,7 @@ class ImmichAssetGridView extends StatefulWidget {
this.showDragScroll = true, this.showDragScroll = true,
this.showStack = false, this.showStack = false,
this.isOwner = true, this.isOwner = true,
this.sharedAlbumId,
}); });
@override @override
@ -141,6 +144,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
heroOffset: widget.heroOffset, heroOffset: widget.heroOffset,
showStack: widget.showStack, showStack: widget.showStack,
isOwner: widget.isOwner, isOwner: widget.isOwner,
sharedAlbumId: widget.sharedAlbumId,
); );
} }
@ -221,7 +225,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
style: TextStyle( style: TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.displayLarge?.color, color: context.textTheme.displayLarge?.color,
), ),
), ),
); );
@ -369,7 +373,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
scrollStateListener: dragScrolling, scrollStateListener: dragScrolling,
itemPositionsListener: _itemPositionsListener, itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController, controller: _itemScrollController,
backgroundColor: Theme.of(context).hintColor, backgroundColor: context.themeData.hintColor,
labelTextBuilder: _labelBuilder, labelTextBuilder: _labelBuilder,
labelConstraints: const BoxConstraints(maxHeight: 28), labelConstraints: const BoxConstraints(maxHeight: 28),
scrollbarAnimationDuration: const Duration(milliseconds: 300), scrollbarAnimationDuration: const Duration(milliseconds: 300),

View file

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
@ -21,6 +21,7 @@ class ThumbnailImage extends StatelessWidget {
final Function? onSelect; final Function? onSelect;
final Function? onDeselect; final Function? onDeselect;
final int heroOffset; final int heroOffset;
final String? sharedAlbumId;
const ThumbnailImage({ const ThumbnailImage({
Key? key, Key? key,
@ -31,6 +32,7 @@ class ThumbnailImage extends StatelessWidget {
this.showStorageIndicator = true, this.showStorageIndicator = true,
this.showStack = false, this.showStack = false,
this.isOwner = true, this.isOwner = true,
this.sharedAlbumId,
this.useGrayBoxPlaceholder = false, this.useGrayBoxPlaceholder = false,
this.isSelected = false, this.isSelected = false,
this.multiselectEnabled = false, this.multiselectEnabled = false,
@ -41,9 +43,9 @@ class ThumbnailImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final assetContainerColor = context.isDarkTheme
final assetContainerColor = ? Colors.blueGrey
isDarkTheme ? Colors.blueGrey : Theme.of(context).primaryColorLight; : context.themeData.primaryColorLight;
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = asset.id == Isar.autoIncrement; final isFromDto = asset.id == Isar.autoIncrement;
@ -56,7 +58,7 @@ class ThumbnailImage extends StatelessWidget {
), ),
child: Icon( child: Icon(
Icons.check_circle_rounded, Icons.check_circle_rounded,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
); );
} else { } else {
@ -176,7 +178,7 @@ class ThumbnailImage extends StatelessWidget {
onSelect?.call(); onSelect?.call();
} }
} else { } else {
AutoRouter.of(context).push( context.autoPush(
GalleryViewerRoute( GalleryViewerRoute(
initialIndex: index, initialIndex: index,
loadAsset: loadAsset, loadAsset: loadAsset,
@ -184,6 +186,7 @@ class ThumbnailImage extends StatelessWidget {
heroOffset: heroOffset, heroOffset: heroOffset,
showStack: showStack, showStack: showStack,
isOwner: isOwner, isOwner: isOwner,
sharedAlbumId: sharedAlbumId,
), ),
); );
} }

View file

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart'; import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/modules/home/models/selection_state.dart'; import 'package:immich_mobile/modules/home/models/selection_state.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
@ -44,7 +45,6 @@ class ControlBottomAppBar extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
var hasRemote = var hasRemote =
selectionAssetState.hasRemote || selectionAssetState.hasMerged; selectionAssetState.hasRemote || selectionAssetState.hasMerged;
var hasLocal = var hasLocal =
@ -161,7 +161,7 @@ class ControlBottomAppBar extends ConsumerWidget {
ScrollController scrollController, ScrollController scrollController,
) { ) {
return Card( return Card(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100], color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100],
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
elevation: 18.0, elevation: 18.0,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@ -244,12 +244,12 @@ class AddToAlbumTitleRow extends StatelessWidget {
onPressed: onCreateNewAlbum, onPressed: onCreateNewAlbum,
icon: Icon( icon: Icon(
Icons.add, Icons.add,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
label: Text( label: Text(
"common_create_new_album", "common_create_new_album",
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
), ),

View file

@ -1,12 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.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:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@ -106,8 +106,7 @@ class HomePage extends HookConsumerWidget {
handleShareAssets(ref, context, selection.value.toList()); handleShareAssets(ref, context, selection.value.toList());
} else { } else {
final ids = remoteOnlySelection().map((e) => e.remoteId!); final ids = remoteOnlySelection().map((e) => e.remoteId!);
AutoRouter.of(context) context.autoPush(SharedLinkEditRoute(assetsList: ids.toList()));
.push(SharedLinkEditRoute(assetsList: ids.toList()));
} }
processing.value = false; processing.value = false;
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
@ -265,7 +264,7 @@ class HomePage extends HookConsumerWidget {
ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id)); context.autoPush(AlbumViewerRoute(albumId: result.id));
} }
} finally { } finally {
processing.value = false; processing.value = false;
@ -322,7 +321,7 @@ class HomePage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 16, fontSize: 16,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
).tr(), ).tr(),
), ),

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart'; import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@ -37,7 +38,7 @@ class ChangePasswordForm extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
Padding( Padding(
@ -191,7 +192,7 @@ class ChangePasswordButton extends ConsumerWidget {
return ElevatedButton( return ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.standard, visualDensity: VisualDensity.standard,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: context.primaryColor,
foregroundColor: Colors.grey[50], foregroundColor: Colors.grey[50],
elevation: 2, elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),

View file

@ -1,9 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/login/providers/oauth.provider.dart'; import 'package:immich_mobile/modules/login/providers/oauth.provider.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -150,7 +150,7 @@ class LoginForm extends HookConsumerWidget {
// Resume backup (if enable) then navigate // Resume backup (if enable) then navigate
if (ref.read(authenticationProvider).shouldChangePassword && if (ref.read(authenticationProvider).shouldChangePassword &&
!ref.read(authenticationProvider).isAdmin) { !ref.read(authenticationProvider).isAdmin) {
AutoRouter.of(context).push(const ChangePasswordRoute()); context.autoPush(const ChangePasswordRoute());
} else { } else {
final hasPermission = await ref final hasPermission = await ref
.read(galleryPermissionNotifier.notifier) .read(galleryPermissionNotifier.notifier)
@ -159,7 +159,7 @@ class LoginForm extends HookConsumerWidget {
// Don't resume the backup until we have gallery permission // Don't resume the backup until we have gallery permission
ref.read(backupProvider.notifier).resumeBackup(); ref.read(backupProvider.notifier).resumeBackup();
} }
AutoRouter.of(context).replace(const TabControllerRoute()); context.autoReplace(const TabControllerRoute());
} }
} else { } else {
ImmichToast.show( ImmichToast.show(
@ -212,9 +212,7 @@ class LoginForm extends HookConsumerWidget {
if (permission.isGranted || permission.isLimited) { if (permission.isGranted || permission.isLimited) {
ref.watch(backupProvider.notifier).resumeBackup(); ref.watch(backupProvider.notifier).resumeBackup();
} }
AutoRouter.of(context).replace( context.autoReplace(const TabControllerRoute());
const TabControllerRoute(),
);
} else { } else {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
@ -260,8 +258,7 @@ class LoginForm extends HookConsumerWidget {
), ),
), ),
), ),
onPressed: () => onPressed: () => context.autoPush(const SettingsRoute()),
AutoRouter.of(context).push(const SettingsRoute()),
icon: const Icon(Icons.settings_rounded), icon: const Icon(Icons.settings_rounded),
label: const SizedBox.shrink(), label: const SizedBox.shrink(),
), ),
@ -303,7 +300,7 @@ class LoginForm extends HookConsumerWidget {
children: [ children: [
Text( Text(
serverEndpointController.text, serverEndpointController.text,
style: Theme.of(context).textTheme.displaySmall, style: context.textTheme.displaySmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
if (isPasswordLoginEnable.value) ...[ if (isPasswordLoginEnable.value) ...[
@ -339,8 +336,7 @@ class LoginForm extends HookConsumerWidget {
horizontal: 16.0, horizontal: 16.0,
), ),
child: Divider( child: Divider(
color: Brightness.dark == color: context.isDarkTheme
Theme.of(context).brightness
? Colors.white ? Colors.white
: Colors.black, : Colors.black,
), ),
@ -588,7 +584,7 @@ class OAuthLoginButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon( return ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor.withAlpha(230), backgroundColor: context.primaryColor.withAlpha(230),
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
), ),
onPressed: onPressed, onPressed: onPressed,

View file

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/login/ui/login_form.dart'; import 'package:immich_mobile/modules/login/ui/login_form.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@ -47,13 +47,13 @@ class LoginPage extends HookConsumerWidget {
child: Text( child: Text(
'Logs', 'Logs',
style: TextStyle( style: TextStyle(
color: Theme.of(context).primaryColor, color: context.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontFamily: "Inconsolata", fontFamily: "Inconsolata",
), ),
), ),
onTap: () { onTap: () {
AutoRouter.of(context).push(const AppLogRoute()); context.autoPush(const AppLogRoute());
}, },
), ),
], ],

View file

@ -1,14 +1,20 @@
import 'package:vector_map_tiles/vector_map_tiles.dart';
class MapState { class MapState {
final bool isDarkTheme; final bool isDarkTheme;
final bool showFavoriteOnly; final bool showFavoriteOnly;
final bool includeArchived; final bool includeArchived;
final int relativeTime; final int relativeTime;
final Style? mapStyle;
final bool isLoading;
MapState({ MapState({
this.isDarkTheme = false, this.isDarkTheme = false,
this.showFavoriteOnly = false, this.showFavoriteOnly = false,
this.includeArchived = false, this.includeArchived = false,
this.relativeTime = 0, this.relativeTime = 0,
this.mapStyle,
this.isLoading = false,
}); });
MapState copyWith({ MapState copyWith({
@ -16,18 +22,22 @@ class MapState {
bool? showFavoriteOnly, bool? showFavoriteOnly,
bool? includeArchived, bool? includeArchived,
int? relativeTime, int? relativeTime,
Style? mapStyle,
bool? isLoading,
}) { }) {
return MapState( return MapState(
isDarkTheme: isDarkTheme ?? this.isDarkTheme, isDarkTheme: isDarkTheme ?? this.isDarkTheme,
showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly, showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly,
includeArchived: includeArchived ?? this.includeArchived, includeArchived: includeArchived ?? this.includeArchived,
relativeTime: relativeTime ?? this.relativeTime, relativeTime: relativeTime ?? this.relativeTime,
mapStyle: mapStyle ?? this.mapStyle,
isLoading: isLoading ?? this.isLoading,
); );
} }
@override @override
String toString() { String toString() {
return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived)'; return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived, mapStyle: $mapStyle, isLoading: $isLoading)';
} }
@override @override
@ -38,7 +48,9 @@ class MapState {
other.isDarkTheme == isDarkTheme && other.isDarkTheme == isDarkTheme &&
other.showFavoriteOnly == showFavoriteOnly && other.showFavoriteOnly == showFavoriteOnly &&
other.relativeTime == relativeTime && other.relativeTime == relativeTime &&
other.includeArchived == includeArchived; other.includeArchived == includeArchived &&
other.mapStyle == mapStyle &&
other.isLoading == isLoading;
} }
@override @override
@ -46,6 +58,8 @@ class MapState {
return isDarkTheme.hashCode ^ return isDarkTheme.hashCode ^
showFavoriteOnly.hashCode ^ showFavoriteOnly.hashCode ^
relativeTime.hashCode ^ relativeTime.hashCode ^
includeArchived.hashCode; includeArchived.hashCode ^
mapStyle.hashCode ^
isLoading.hashCode;
} }
} }

View file

@ -1,10 +1,23 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/map/models/map_state.model.dart'; import 'package:immich_mobile/modules/map/models/map_state.model.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/utils/color_filter_generator.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:vector_map_tiles/vector_map_tiles.dart';
class MapStateNotifier extends StateNotifier<MapState> { class MapStateNotifier extends StateNotifier<MapState> {
MapStateNotifier(this._appSettingsProvider) MapStateNotifier(this._appSettingsProvider, this._apiService)
: super( : super(
MapState( MapState(
isDarkTheme: _appSettingsProvider isDarkTheme: _appSettingsProvider
@ -15,17 +28,69 @@ class MapStateNotifier extends StateNotifier<MapState> {
.getSetting<bool>(AppSettingsEnum.mapIncludeArchived), .getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
relativeTime: _appSettingsProvider relativeTime: _appSettingsProvider
.getSetting<int>(AppSettingsEnum.mapRelativeDate), .getSetting<int>(AppSettingsEnum.mapRelativeDate),
isLoading: true,
), ),
) {
_fetchStyleFromServer(
_appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapThemeMode),
); );
}
final AppSettingsService _appSettingsProvider; final AppSettingsService _appSettingsProvider;
final ApiService _apiService;
final Logger _log = Logger("MapStateNotifier");
bool get isRaster =>
state.mapStyle != null && state.mapStyle!.rasterTileProvider != null;
double get maxZoom =>
(isRaster ? state.mapStyle!.rasterTileProvider!.maximumZoom : 14)
.toDouble();
void switchTheme(bool isDarkTheme) { void switchTheme(bool isDarkTheme) {
_updateThemeMode(isDarkTheme);
_fetchStyleFromServer(isDarkTheme);
}
void _updateThemeMode(bool isDarkTheme) {
_appSettingsProvider.setSetting( _appSettingsProvider.setSetting(
AppSettingsEnum.mapThemeMode, AppSettingsEnum.mapThemeMode,
isDarkTheme, isDarkTheme,
); );
state = state.copyWith(isDarkTheme: isDarkTheme); state = state.copyWith(isDarkTheme: isDarkTheme, isLoading: true);
}
void _fetchStyleFromServer(bool isDarkTheme) async {
final styleResponse = await _apiService.systemConfigApi
.getMapStyleWithHttpInfo(isDarkTheme ? MapTheme.dark : MapTheme.light);
if (styleResponse.statusCode >= HttpStatus.badRequest) {
throw ApiException(styleResponse.statusCode, styleResponse.body);
}
final styleJsonString = styleResponse.body.isNotEmpty &&
styleResponse.statusCode != HttpStatus.noContent
? styleResponse.body
: null;
if (styleJsonString == null) {
_log.severe('Style JSON from server is empty');
return;
}
final styleJson = await compute(jsonDecode, styleJsonString);
if (styleJson is! Map<String, dynamic>) {
_log.severe('Style JSON from server is invalid');
return;
}
final styleReader = StyleReader(uri: '');
Style? style;
try {
style = await styleReader.readFromMap(styleJson);
} finally {
// Consume all error
}
state = state.copyWith(
mapStyle: style,
isLoading: false,
);
} }
void switchFavoriteOnly(bool isFavoriteOnly) { void switchFavoriteOnly(bool isFavoriteOnly) {
@ -51,9 +116,44 @@ class MapStateNotifier extends StateNotifier<MapState> {
); );
state = state.copyWith(relativeTime: relativeTime); state = state.copyWith(relativeTime: relativeTime);
} }
Widget getTileLayer([bool forceDark = false]) {
if (isRaster) {
final rasterProvider = state.mapStyle!.rasterTileProvider;
final rasterLayer = TileLayer(
urlTemplate: rasterProvider!.url,
maxNativeZoom: rasterProvider.maximumZoom,
maxZoom: rasterProvider.maximumZoom.toDouble(),
);
return state.isDarkTheme || forceDark
? InvertionFilter(
child: SaturationFilter(
saturation: -1,
child: BrightnessFilter(
brightness: -1,
child: rasterLayer,
),
),
)
: rasterLayer;
}
if (state.mapStyle != null && !isRaster) {
return VectorTileLayer(
// Tiles and themes will be set for vector providers
tileProviders: state.mapStyle!.providers!,
theme: state.mapStyle!.theme!,
sprites: state.mapStyle!.sprites,
concurrency: 6,
);
}
return const Center(child: ImmichLoadingIndicator());
}
} }
final mapStateNotifier = final mapStateNotifier =
StateNotifierProvider<MapStateNotifier, MapState>((ref) { StateNotifierProvider<MapStateNotifier, MapState>((ref) {
return MapStateNotifier(ref.watch(appSettingsServiceProvider)); return MapStateNotifier(
ref.watch(appSettingsServiceProvider),
ref.watch(apiServiceProvider),
);
}); });

View file

@ -1,8 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.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/modules/home/ui/asset_grid/disable_multi_select_button.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/disable_multi_select_button.dart';
import 'package:immich_mobile/modules/map/ui/map_settings_dialog.dart'; import 'package:immich_mobile/modules/map/ui/map_settings_dialog.dart';
@ -30,7 +30,7 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget {
Padding( Padding(
padding: const EdgeInsets.only(left: 15, top: 15), padding: const EdgeInsets.only(left: 15, top: 15),
child: ElevatedButton( child: ElevatedButton(
onPressed: () => AutoRouter.of(context).pop(), onPressed: () => context.autoPop(),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: const CircleBorder(), shape: const CircleBorder(),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),

View file

@ -5,6 +5,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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
@ -15,7 +16,6 @@ import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/utils/color_filter_generator.dart'; import 'package:immich_mobile/utils/color_filter_generator.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:url_launcher/url_launcher.dart';
class MapPageBottomSheet extends StatefulHookConsumerWidget { class MapPageBottomSheet extends StatefulHookConsumerWidget {
final Stream mapPageEventStream; final Stream mapPageEventStream;
@ -57,10 +57,10 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkTheme = context.isDarkTheme;
final bottomPadding = final bottomPadding =
Platform.isAndroid ? MediaQuery.of(context).padding.bottom - 10 : 0.0; Platform.isAndroid ? MediaQuery.of(context).padding.bottom - 10 : 0.0;
final maxHeight = MediaQuery.of(context).size.height - bottomPadding; final maxHeight = context.height - bottomPadding;
final isSheetScrolled = useState(false); final isSheetScrolled = useState(false);
final isSheetExpanded = useState(false); final isSheetExpanded = useState(false);
final assetsInBound = useState(<Asset>[]); final assetsInBound = useState(<Asset>[]);
@ -137,7 +137,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
SizedBox( SizedBox(
height: 150, height: 150,
width: 150, width: 150,
child: isDarkMode child: isDarkTheme
? const InvertionFilter( ? const InvertionFilter(
child: SaturationFilter( child: SaturationFilter(
saturation: -1, saturation: -1,
@ -156,7 +156,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
"map_zoom_to_see_photos".tr(), "map_zoom_to_see_photos".tr(),
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
color: Theme.of(context).textTheme.displayLarge?.color, color: context.textTheme.displayLarge?.color,
), ),
), ),
], ],
@ -182,7 +182,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
height: 60, height: 60,
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100], color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
), ),
child: Stack( child: Stack(
children: [ children: [
@ -197,17 +197,14 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
textToDisplay, textToDisplay,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Theme.of(context).textTheme.displayLarge?.color, color: context.textTheme.displayLarge?.color,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Divider( Divider(
height: 10, height: 10,
color: Theme.of(context) color:
.textTheme context.textTheme.displayLarge?.color?.withOpacity(0.5),
.displayLarge
?.color
?.withOpacity(0.5),
), ),
], ],
), ),
@ -218,7 +215,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
child: IconButton( child: IconButton(
icon: Icon( icon: Icon(
Icons.map_outlined, Icons.map_outlined,
color: Theme.of(context).textTheme.displayLarge?.color, color: context.textTheme.displayLarge?.color,
), ),
iconSize: 20, iconSize: 20,
tooltip: 'Zoom to bounds', tooltip: 'Zoom to bounds',
@ -266,7 +263,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
ScrollController scrollController, ScrollController scrollController,
) { ) {
return Card( return Card(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100], color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,
elevation: 18.0, elevation: 18.0,
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
@ -320,18 +317,13 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
Positioned( Positioned(
bottom: maxHeight * currentExtend.value, bottom: maxHeight * currentExtend.value,
left: 0, left: 0,
child: GestureDetector(
onTap: () => launchUrl(
Uri.parse('https://openstreetmap.org/copyright'),
),
child: ColoredBox( child: ColoredBox(
color: (widget.isDarkTheme color:
? Colors.grey[900] (widget.isDarkTheme ? Colors.grey[900] : Colors.grey[100])!,
: Colors.grey[100])!,
child: Padding( child: Padding(
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
child: Text( child: Text(
'© OpenStreetMap contributors', 'OpenStreetMap contributors',
style: TextStyle( style: TextStyle(
fontSize: 6, fontSize: 6,
color: !widget.isDarkTheme color: !widget.isDarkTheme
@ -342,7 +334,6 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
), ),
), ),
), ),
),
Positioned( Positioned(
bottom: maxHeight * (0.14 + (currentExtend.value - 0.1)), bottom: maxHeight * (0.14 + (currentExtend.value - 0.1)),
right: 15, right: 15,

View file

@ -2,6 +2,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:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart'; import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
class MapSettingsDialog extends HookConsumerWidget { class MapSettingsDialog extends HookConsumerWidget {
@ -15,7 +16,7 @@ class MapSettingsDialog extends HookConsumerWidget {
final showFavoriteOnly = useState(mapSettings.showFavoriteOnly); final showFavoriteOnly = useState(mapSettings.showFavoriteOnly);
final showIncludeArchived = useState(mapSettings.includeArchived); final showIncludeArchived = useState(mapSettings.includeArchived);
final showRelativeDate = useState(mapSettings.relativeTime); final showRelativeDate = useState(mapSettings.relativeTime);
final ThemeData theme = Theme.of(context); final ThemeData theme = context.themeData;
Widget buildMapThemeSetting() { Widget buildMapThemeSetting() {
return SwitchListTile.adaptive( return SwitchListTile.adaptive(
@ -125,7 +126,7 @@ class MapSettingsDialog extends HookConsumerWidget {
List<Widget> getDialogActions() { List<Widget> getDialogActions() {
return <Widget>[ return <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => context.pop(),
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: backgroundColor:
mapSettings.isDarkTheme ? Colors.grey[100] : Colors.grey[700], mapSettings.isDarkTheme ? Colors.grey[100] : Colors.grey[700],
@ -146,7 +147,7 @@ class MapSettingsDialog extends HookConsumerWidget {
mapSettingsNotifier.setRelativeTime(showRelativeDate.value); mapSettingsNotifier.setRelativeTime(showRelativeDate.value);
mapSettingsNotifier mapSettingsNotifier
.switchIncludeArchived(showIncludeArchived.value); .switchIncludeArchived(showIncludeArchived.value);
Navigator.of(context).pop(); context.pop();
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: theme.primaryColor, backgroundColor: theme.primaryColor,
@ -178,7 +179,7 @@ class MapSettingsDialog extends HookConsumerWidget {
width: double.maxFinite, width: double.maxFinite,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.6, maxHeight: context.height * 0.6,
), ),
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map/plugin_api.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
import 'package:immich_mobile/utils/color_filter_generator.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -29,11 +28,7 @@ class MapThumbnail extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tileLayer = TileLayer( ref.watch(mapStateNotifier.select((s) => s.mapStyle));
urlTemplate: ref.watch(
serverInfoProvider.select((v) => v.serverConfig.mapTileUrl),
),
);
return SizedBox( return SizedBox(
height: height, height: height,
@ -55,20 +50,14 @@ class MapThumbnail extends HookConsumerWidget {
'OpenStreetMap contributors', 'OpenStreetMap contributors',
onTap: () => launchUrl( onTap: () => launchUrl(
Uri.parse('https://openstreetmap.org/copyright'), Uri.parse('https://openstreetmap.org/copyright'),
mode: LaunchMode.externalApplication,
), ),
), ),
], ],
), ),
], ],
children: [ children: [
isDarkTheme ref.read(mapStateNotifier.notifier).getTileLayer(isDarkTheme),
? InvertionFilter(
child: SaturationFilter(
saturation: -1,
child: tileLayer,
),
)
: tileLayer,
if (markers.isNotEmpty) MarkerLayer(markers: markers), if (markers.isNotEmpty) MarkerLayer(markers: markers),
], ],
), ),

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,6 +11,7 @@ import 'package:flutter_map_heatmap/flutter_map_heatmap.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/map/models/map_page_event.model.dart'; import 'package:immich_mobile/modules/map/models/map_page_event.model.dart';
import 'package:immich_mobile/modules/map/providers/map_marker.provider.dart'; import 'package:immich_mobile/modules/map/providers/map_marker.provider.dart';
import 'package:immich_mobile/modules/map/providers/map_state.provider.dart'; import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
@ -20,12 +21,10 @@ import 'package:immich_mobile/modules/map/ui/map_page_bottom_sheet.dart';
import 'package:immich_mobile/modules/map/ui/map_page_app_bar.dart'; import 'package:immich_mobile/modules/map/ui/map_page_app_bar.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/color_filter_generator.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/utils/flutter_map_extensions.dart'; import 'package:immich_mobile/extensions/flutter_map_extensions.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'package:immich_mobile/utils/selection_handlers.dart'; import 'package:immich_mobile/utils/selection_handlers.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -79,6 +78,7 @@ class MapPageState extends ConsumerState<MapPage> {
Set<AssetMarkerData>? assetMarkers, { Set<AssetMarkerData>? assetMarkers, {
bool forceReload = false, bool forceReload = false,
}) { }) {
try {
final bounds = mapController.bounds; final bounds = mapController.bounds;
if (bounds != null) { if (bounds != null) {
final oldAssetsInBounds = assetsInBounds.toSet(); final oldAssetsInBounds = assetsInBounds.toSet();
@ -95,10 +95,13 @@ class MapPageState extends ConsumerState<MapPage> {
); );
} }
} }
} finally {
// Consume all error
}
} }
void openAssetInViewer(Asset asset) { void openAssetInViewer(Asset asset) {
AutoRouter.of(context).push( context.autoPush(
GalleryViewerRoute( GalleryViewerRoute(
initialIndex: 0, initialIndex: 0,
loadAsset: (index) => asset, loadAsset: (index) => asset,
@ -120,6 +123,10 @@ class MapPageState extends ConsumerState<MapPage> {
final selectedAssets = useState(<Asset>{}); final selectedAssets = useState(<Asset>{});
final showLoadingIndicator = useState(false); final showLoadingIndicator = useState(false);
final refetchMarkers = useState(true); final refetchMarkers = useState(true);
final isLoading =
ref.watch(mapStateNotifier.select((state) => state.isLoading));
final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
final zoomLevel = math.min(maxZoom, 14.0);
if (refetchMarkers.value) { if (refetchMarkers.value) {
mapMarkerData.value = ref.watch(mapMarkersProvider).when( mapMarkerData.value = ref.watch(mapMarkersProvider).when(
@ -168,7 +175,6 @@ class MapPageState extends ConsumerState<MapPage> {
final mapMarker = mapMarkerData.value final mapMarker = mapMarkerData.value
.firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id); .firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id);
if (mapMarker != null) { if (mapMarker != null) {
const zoomLevel = 16.0;
LatLng? newCenter = mapController.centerBoundsWithPadding( LatLng? newCenter = mapController.centerBoundsWithPadding(
mapMarker.point, mapMarker.point,
const Offset(0, -120), const Offset(0, -120),
@ -230,7 +236,7 @@ class MapPageState extends ConsumerState<MapPage> {
forceAssetUpdate = true; forceAssetUpdate = true;
mapController.move( mapController.move(
LatLng(currentUserLocation.latitude, currentUserLocation.longitude), LatLng(currentUserLocation.latitude, currentUserLocation.longitude),
12, zoomLevel,
); );
} catch (error) { } catch (error) {
log.severe( log.severe(
@ -359,24 +365,6 @@ class MapPageState extends ConsumerState<MapPage> {
selectedAssets.value = selection; selectedAssets.value = selection;
} }
final tileLayer = TileLayer(
urlTemplate: ref.watch(
serverInfoProvider.select((v) => v.serverConfig.mapTileUrl),
),
maxNativeZoom: 19,
maxZoom: 19,
);
final darkTileLayer = InvertionFilter(
child: SaturationFilter(
saturation: -1,
child: BrightnessFilter(
brightness: -1,
child: tileLayer,
),
),
);
final markerLayer = MarkerLayer( final markerLayer = MarkerLayer(
markers: [ markers: [
if (closestAssetMarker.value != null) if (closestAssetMarker.value != null)
@ -451,6 +439,7 @@ class MapPageState extends ConsumerState<MapPage> {
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
body: Stack( body: Stack(
children: [ children: [
if (!isLoading)
FlutterMap( FlutterMap(
mapController: mapController, mapController: mapController,
options: MapOptions( options: MapOptions(
@ -464,17 +453,18 @@ class MapPageState extends ConsumerState<MapPage> {
center: LatLng(20, 20), center: LatLng(20, 20),
zoom: 2, zoom: 2,
minZoom: 1, minZoom: 1,
maxZoom: 18, // max level supported by OSM, maxZoom: maxZoom,
onMapReady: () { onMapReady: () {
mapController.mapEventStream.listen(onMapEvent); mapController.mapEventStream.listen(onMapEvent);
}, },
), ),
children: [ children: [
isDarkTheme ? darkTileLayer : tileLayer, ref.read(mapStateNotifier.notifier).getTileLayer(),
heatMapLayer, heatMapLayer,
markerLayer, markerLayer,
], ],
), ),
if (!isLoading)
MapPageBottomSheet( MapPageBottomSheet(
mapPageEventStream: mapPageEventSC.stream, mapPageEventStream: mapPageEventSC.stream,
bottomSheetEventSC: bottomSheetEventSC, bottomSheetEventSC: bottomSheetEventSC,
@ -482,10 +472,10 @@ class MapPageState extends ConsumerState<MapPage> {
selectionlistener: selectionListener, selectionlistener: selectionListener,
isDarkTheme: isDarkTheme, isDarkTheme: isDarkTheme,
), ),
if (showLoadingIndicator.value) if (showLoadingIndicator.value || isLoading)
Positioned( Positioned(
top: MediaQuery.of(context).size.height * 0.35, top: context.height * 0.35,
left: MediaQuery.of(context).size.width * 0.425, left: context.width * 0.425,
child: const ImmichLoadingIndicator(), child: const ImmichLoadingIndicator(),
), ),
], ],

View file

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart'; import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
@ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget {
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
AutoRouter.of(context).push( context.autoPush(
MemoryRoute( MemoryRoute(
memories: memories, memories: memories,
memoryIndex: index, memoryIndex: index,

View file

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/memories/models/memory.dart'; import 'package:immich_mobile/modules/memories/models/memory.dart';
import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; import 'package:immich_mobile/modules/memories/ui/memory_card.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
@ -182,14 +182,14 @@ class MemoryPage extends HookConsumerWidget {
currentMemory.value.assets.length; currentMemory.value.assets.length;
if (isLastAsset && if (isLastAsset &&
(offset > notification.metrics.maxScrollExtent + 150)) { (offset > notification.metrics.maxScrollExtent + 150)) {
AutoRouter.of(context).pop(); context.autoPop();
return true; return true;
} }
} }
// Horizontal scroll handling // Horizontal scroll handling
if (notification.depth == 1 && if (notification.depth == 1 &&
(offset > notification.metrics.maxScrollExtent + 100)) { (offset > notification.metrics.maxScrollExtent + 100)) {
AutoRouter.of(context).pop(); context.autoPop();
return true; return true;
} }
} }
@ -244,7 +244,7 @@ class MemoryPage extends HookConsumerWidget {
child: MemoryCard( child: MemoryCard(
asset: asset, asset: asset,
onTap: () => toNextAsset(index), onTap: () => toNextAsset(index),
onClose: () => AutoRouter.of(context).pop(), onClose: () => context.autoPop(),
rightCornerText: assetProgress.value, rightCornerText: assetProgress.value,
title: memories[mIndex].title, title: memories[mIndex].title,
showTitle: index == 0, showTitle: index == 0,

View file

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
@ -11,7 +11,6 @@ import 'package:immich_mobile/shared/ui/immich_title_text.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class PermissionOnboardingPage extends HookConsumerWidget { class PermissionOnboardingPage extends HookConsumerWidget {
const PermissionOnboardingPage({super.key}); const PermissionOnboardingPage({super.key});
@override @override
@ -21,13 +20,10 @@ class PermissionOnboardingPage extends HookConsumerWidget {
// Navigate to the main Tab Controller when permission is granted // Navigate to the main Tab Controller when permission is granted
void goToHome() { void goToHome() {
// Resume backup (if enable) then navigate // Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup() ref.watch(backupProvider.notifier).resumeBackup().catchError((error) {
.catchError((error) {
debugPrint('PermissionOnboardingPage error: $error'); debugPrint('PermissionOnboardingPage error: $error');
}); });
AutoRouter.of(context).replace( context.autoReplace(const TabControllerRoute());
const TabControllerRoute(),
);
} }
// When the permission is denied, we show a request permission page // When the permission is denied, we show a request permission page
@ -38,7 +34,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
children: [ children: [
Text( Text(
'permission_onboarding_request', 'permission_onboarding_request',
style: Theme.of(context).textTheme.titleMedium, style: context.textTheme.titleMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),
const SizedBox(height: 18), const SizedBox(height: 18),
@ -70,7 +66,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
children: [ children: [
Text( Text(
'permission_onboarding_permission_granted', 'permission_onboarding_permission_granted',
style: Theme.of(context).textTheme.titleMedium, style: context.textTheme.titleMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),
const SizedBox(height: 18), const SizedBox(height: 18),
@ -90,14 +86,15 @@ class PermissionOnboardingPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.warning_outlined, const Icon(
Icons.warning_outlined,
color: Colors.yellow, color: Colors.yellow,
size: 48, size: 48,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'permission_onboarding_permission_limited', 'permission_onboarding_permission_limited',
style: Theme.of(context).textTheme.titleMedium, style: context.textTheme.titleMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),
const SizedBox(height: 18), const SizedBox(height: 18),
@ -123,14 +120,15 @@ class PermissionOnboardingPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.warning_outlined, const Icon(
Icons.warning_outlined,
color: Colors.red, color: Colors.red,
size: 48, size: 48,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'permission_onboarding_permission_denied', 'permission_onboarding_permission_denied',
style: Theme.of(context).textTheme.titleMedium, style: context.textTheme.titleMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),
const SizedBox(height: 18), const SizedBox(height: 18),
@ -186,13 +184,10 @@ class PermissionOnboardingPage extends HookConsumerWidget {
child: const Text('permission_onboarding_log_out').tr(), child: const Text('permission_onboarding_log_out').tr(),
onPressed: () { onPressed: () {
ref.read(authenticationProvider.notifier).logout(); ref.read(authenticationProvider.notifier).logout();
AutoRouter.of(context).replace( context.autoReplace(const LoginRoute());
const LoginRoute(),
);
}, },
), ),
], ],
), ),
), ),
), ),

View file

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/ui/user_avatar.dart'; import 'package:immich_mobile/shared/ui/user_avatar.dart';
@ -28,10 +28,10 @@ class PartnerList extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
onTap: () => AutoRouter.of(context).push(PartnerDetailRoute(partner: p)), onTap: () => context.autoPush((PartnerDetailRoute(partner: p))),
); );
} }
} }

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
@ -85,7 +86,7 @@ class CuratedPeopleRow extends StatelessWidget {
"Add name", "Add name",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: context.primaryColor,
), ),
), ),
), ),

View file

@ -1,5 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart'; import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
import 'package:immich_mobile/modules/search/ui/curated_row.dart'; import 'package:immich_mobile/modules/search/ui/curated_row.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
@ -25,7 +25,7 @@ class CuratedPlacesRow extends CuratedRow {
final int actualContentIndex = isMapEnabled ? 1 : 0; final int actualContentIndex = isMapEnabled ? 1 : 0;
Widget buildMapThumbnail() { Widget buildMapThumbnail() {
return GestureDetector( return GestureDetector(
onTap: () => AutoRouter.of(context).push( onTap: () => context.autoPush(
const MapRoute(), const MapRoute(),
), ),
child: SizedBox( child: SizedBox(
@ -43,10 +43,12 @@ class CuratedPlacesRow extends CuratedRow {
), ),
height: imageSize, height: imageSize,
showAttribution: false, showAttribution: false,
isDarkTheme: Theme.of(context).brightness == Brightness.dark, isDarkTheme: context.isDarkTheme,
), ),
), ),
Container( Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
color: Colors.black, color: Colors.black,
@ -57,7 +59,8 @@ class CuratedPlacesRow extends CuratedRow {
Colors.blueGrey.withOpacity(0.0), Colors.blueGrey.withOpacity(0.0),
Colors.black.withOpacity(0.4), Colors.black.withOpacity(0.4),
], ],
stops: const [0.0, 1.0], stops: const [0.0, 0.4],
),
), ),
), ),
), ),

View file

@ -1,5 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart';
import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -50,13 +50,13 @@ class ExploreGrid extends StatelessWidget {
borderRadius: 0, borderRadius: 0,
onTap: () { onTap: () {
isPeople isPeople
? AutoRouter.of(context).push( ? context.autoPush(
PersonResultRoute( PersonResultRoute(
personId: content.id, personId: content.id,
personName: content.label, personName: content.label,
), ),
) )
: AutoRouter.of(context).push( : context.autoPush(
SearchResultRoute(searchTerm: 'm:${content.label}'), SearchResultRoute(searchTerm: 'm:${content.label}'),
); );
}, },

Some files were not shown because too many files have changed in this diff Show more