Compare commits

..

1 Commits

Author SHA1 Message Date
mertalev
e62071fda0 don't use iterator.find 2025-06-19 17:40:56 +03:00
252 changed files with 3557 additions and 3324 deletions

View File

@@ -461,7 +461,7 @@ jobs:
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
run: npx playwright install chromium --only-shell
run: npx playwright install --with-deps chromium
if: ${{ !cancelled() }}
- name: Docker build
@@ -644,7 +644,7 @@ jobs:
contents: read
services:
postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"chokidar": "^4.0.3",
@@ -54,7 +54,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.3",
"version": "1.135.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -122,7 +122,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
env_file:
- .env
environment:

View File

@@ -63,7 +63,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
env_file:
- .env
environment:

View File

@@ -56,7 +56,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}

View File

@@ -39,8 +39,8 @@ alt="Dot Env Example"
/>
- Change the default `DB_PASSWORD`, and add custom database connection information if necessary.
- Change `DB_DATA_LOCATION` to a folder (absolute path) where the database will be saved to disk.
- Change `UPLOAD_LOCATION` to a folder (absolute path) where media (uploaded and generated) will be stored.
- Change `DB_DATA_LOCATION` to a folder where the database will be saved to disk.
- Change `UPLOAD_LOCATION` to a folder where media (uploaded and generated) will be stored.
11. Click on "**Deploy the stack**".

View File

@@ -1,16 +1,4 @@
[
{
"label": "v1.135.3",
"url": "https://v1.135.3.archive.immich.app"
},
{
"label": "v1.135.2",
"url": "https://v1.135.2.archive.immich.app"
},
{
"label": "v1.135.1",
"url": "https://v1.135.1.archive.immich.app"
},
{
"label": "v1.135.0",
"url": "https://v1.135.0.archive.immich.app"

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.135.3",
"version": "1.135.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.135.3",
"version": "1.135.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -44,7 +44,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.72",
"version": "2.2.69",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -93,7 +93,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.3",
"version": "1.135.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.135.3",
"version": "1.135.0",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -14,11 +14,7 @@ describe('/people', () => {
let nameAlicePerson: PersonResponseDto;
let nameBobPerson: PersonResponseDto;
let nameCharliePerson: PersonResponseDto;
let nameNullPerson4Assets: PersonResponseDto;
let nameNullPerson3Assets: PersonResponseDto;
let nameNullPerson1Asset: PersonResponseDto;
let nameBillPersonFavourite: PersonResponseDto;
let nameFreddyPersonFavourite: PersonResponseDto;
let nameNullPerson: PersonResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
@@ -31,11 +27,7 @@ describe('/people', () => {
nameCharliePerson,
nameBobPerson,
nameAlicePerson,
nameNullPerson4Assets,
nameNullPerson3Assets,
nameNullPerson1Asset,
nameBillPersonFavourite,
nameFreddyPersonFavourite,
nameNullPerson,
] = await Promise.all([
utils.createPerson(admin.accessToken, {
name: 'visible_person',
@@ -60,26 +52,11 @@ describe('/people', () => {
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: 'Bill',
isFavorite: true,
}),
utils.createPerson(admin.accessToken, {
name: 'Freddy',
isFavorite: true,
}),
]);
const asset1 = await utils.createAsset(admin.accessToken);
const asset2 = await utils.createAsset(admin.accessToken);
const asset3 = await utils.createAsset(admin.accessToken);
const asset4 = await utils.createAsset(admin.accessToken);
await Promise.all([
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
@@ -87,27 +64,15 @@ describe('/people', () => {
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }), // 4 assets
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }),
// Named persons
utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset
utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }),
utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets
utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset
// Null-named person 4 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset4.id, personId: nameNullPerson4Assets.id }), // 4 assets
// Null-named person 3 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson3Assets.id }), // 3 assets
// Null-named person 1 asset
utils.createFace({ assetId: asset3.id, personId: nameNullPerson1Asset.id }),
// Favourite People
utils.createFace({ assetId: asset1.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset2.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset1.id, personId: nameBillPersonFavourite.id }),
// Null-named person
utils.createFace({ assetId: asset1.id, personId: nameNullPerson.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson.id }), // 2 assets
]);
});
@@ -122,19 +87,15 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 11,
total: 7,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
expect.objectContaining({ name: 'hidden_person' }), // Should really be before the null names
expect.objectContaining({ name: 'hidden_person' }),
],
});
});
@@ -144,21 +105,17 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body.hasNextPage).toBe(false);
expect(body.total).toBe(11); // All persons
expect(body.total).toBe(7); // All persons
expect(body.hidden).toBe(1); // 'hidden_person'
const people = body.people as PersonResponseDto[];
expect(people.map((p) => p.id)).toEqual([
nameFreddyPersonFavourite.id, // name: 'Freddy', count: 2
nameBillPersonFavourite.id, // name: 'Bill', count: 1
multipleAssetsPerson.id, // name: 'multiple_assets_person', count: 3
nameBobPerson.id, // name: 'Bob', count: 2
nameAlicePerson.id, // name: 'Alice', count: 1
nameCharliePerson.id, // name: 'Charlie', count: 1
visiblePerson.id, // name: 'visible_person', count: 1
nameNullPerson4Assets.id, // name: '', count: 4
nameNullPerson3Assets.id, // name: '', count: 3
]);
expect(people.some((p) => p.id === hiddenPerson.id)).toBe(false);
@@ -170,18 +127,14 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 11,
total: 7,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
],
});
});
@@ -195,9 +148,9 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: true,
total: 11,
total: 7,
hidden: 1,
people: [expect.objectContaining({ name: 'Alice' })],
people: [expect.objectContaining({ name: 'visible_person' })],
});
});
});

View File

@@ -464,6 +464,7 @@
"assets": "Assets",
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
"assets_added_to_name_count": "Added {count, plural, one {# asset} other {# assets}} to {hasName, select, true {<b>{name}</b>} other {new album}}",
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
"assets_count": "{count, plural, one {# asset} other {# assets}}",
"assets_deleted_permanently": "{count} asset(s) deleted permanently",

View File

@@ -244,6 +244,7 @@ async def load(model: InferenceModel) -> InferenceModel:
async def idle_shutdown_task() -> None:
while True:
log.debug("Checking for inactivity...")
if (
last_called is not None
and not active_requests

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 204,
"android.injected.version.name" => "1.135.3",
"android.injected.version.code" => 201,
"android.injected.version.name" => "1.135.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')

View File

@@ -649,7 +649,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -793,7 +793,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -823,7 +823,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -857,7 +857,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -865,7 +865,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -900,7 +900,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -908,7 +908,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -940,7 +940,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -948,7 +948,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -979,7 +979,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1023,7 +1023,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1064,7 +1064,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -78,7 +78,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.135.1</string>
<string>1.135.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -93,7 +93,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>210</string>
<string>209</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
@@ -115,8 +115,8 @@
</dict>
<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
@@ -168,8 +168,5 @@
<true />
<key>NSFaceIDUsageDescription</key>
<string>We need to use FaceID to allow access to your locked folder</string>
<key>NSLocalNetworkUsageDescription</key>
<string>We need local network permission to connect to the local server using IP address and
allow the casting feature to work</string>
</dict>
</plist>
</plist>

View File

@@ -17,14 +17,6 @@ enum AssetType: String, Codable {
case other = "OTHER"
}
struct ServerWellKnown: Codable {
struct APIInfo: Codable{
let endpoint: String
}
let api: APIInfo
}
struct SearchResult: Codable {
let id: String
let type: AssetType
@@ -65,7 +57,7 @@ class ImmichAPI {
init() async throws {
// fetch the credentials from the UserDefaults store that dart placed here
guard let defaults = UserDefaults(suiteName: "group.app.immich.share"),
var serverURL = defaults.string(forKey: "widget_server_url"),
let serverURL = defaults.string(forKey: "widget_server_url"),
let sessionKey = defaults.string(forKey: "widget_auth_token")
else {
throw WidgetError.noLogin
@@ -74,55 +66,13 @@ class ImmichAPI {
if serverURL == "" || sessionKey == "" {
throw WidgetError.noLogin
}
// migrate the server list value to a JSON array if it is not already
if !serverURL.starts(with: "[") {
let newServerList = "[\"\(serverURL)\"]"
defaults.set(newServerList, forKey: "widget_server_url")
serverURL = newServerList
}
guard let urls = try? JSONDecoder().decode([String].self, from: serverURL.data(using: .utf8)!) else {
throw WidgetError.noLogin
}
for url in urls {
guard let endpointURL = URL(string: url) else { continue }
if let apiURL = await Self.validateServer(at: endpointURL) {
serverConfig = ServerConfig(
serverEndpoint: apiURL.absoluteString,
sessionKey: sessionKey
)
return
}
}
throw WidgetError.fetchFailed
serverConfig = ServerConfig(
serverEndpoint: serverURL,
sessionKey: sessionKey
)
}
private static func validateServer(at endpointURL: URL) async -> URL? {
// build a URL that is only scheme, host, and port
var components = URLComponents()
components.scheme = endpointURL.scheme
components.host = endpointURL.host
components.port = endpointURL.port
guard let baseURL = components.url else { return nil }
var pingURL = baseURL
pingURL.appendPathComponent(".well-known")
pingURL.appendPathComponent("immich")
guard let (serverPingJSON, _) = try? await URLSession.shared.data(from: pingURL) else { return nil }
guard let apiInfo = try? JSONDecoder().decode(ServerWellKnown.self, from: serverPingJSON) else { return nil }
var apiURL = baseURL
apiURL.appendPathComponent(apiInfo.api.endpoint)
return apiURL
}
private func buildRequestURL(
serverConfig: ServerConfig,
endpoint: String,

View File

@@ -20,11 +20,8 @@ struct ImmichMemoryProvider: TimelineProvider {
completion: @escaping @Sendable (ImageEntry) -> Void
) {
Task {
var api: ImmichAPI
do {
api = try await ImmichAPI()
} catch let error as WidgetError {
completion(ImageEntry(date: Date(), image: nil, error: error))
guard let api = try? await ImmichAPI() else {
completion(ImageEntry(date: Date(), image: nil, error: .noLogin))
return
}
@@ -82,13 +79,9 @@ struct ImmichMemoryProvider: TimelineProvider {
Task {
var entries: [ImageEntry] = []
let now = Date()
var api: ImmichAPI
do {
api = try await ImmichAPI()
} catch let error as WidgetError {
entries.append(ImageEntry(date: now, image: nil, error: error))
guard let api = try? await ImmichAPI() else {
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
completion(Timeline(entries: entries, policy: .atEnd))
return
}

View File

@@ -43,7 +43,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
"Choose an album to show images from"
}
@Parameter(title: "Album")
@Parameter(title: "Album", default: NO_ALBUM)
var album: Album?
@Parameter(title: "Show Album Name", default: false)
@@ -63,15 +63,10 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
) async
-> ImageEntry
{
var api: ImmichAPI
do {
api = try await ImmichAPI()
} catch let error as WidgetError {
return ImageEntry(date: Date(), image: nil, error: error)
} catch {
return ImageEntry(date: Date(), image: nil, error: .fetchFailed)
guard let api = try? await ImmichAPI() else {
return ImageEntry(date: Date(), image: nil, error: .noLogin)
}
guard
let randomImage = try? await api.fetchSearchResults(
with: SearchFilters(size: 1)
@@ -105,21 +100,15 @@ struct ImmichRandomProvider: AppIntentTimelineProvider {
let now = Date()
// If we don't have a server config, return an entry with an error
var api: ImmichAPI
do {
api = try await ImmichAPI()
} catch let error as WidgetError {
entries.append(ImageEntry(date: now, image: nil, error: error))
return Timeline(entries: entries, policy: .atEnd)
} catch {
entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed))
guard let api = try? await ImmichAPI() else {
entries.append(ImageEntry(date: now, image: nil, error: .noLogin))
return Timeline(entries: entries, policy: .atEnd)
}
// nil if album is NONE or nil
let albumId =
configuration.album?.id != "NONE" ? configuration.album?.id : nil
let albumName: String? = albumId != nil ? configuration.album?.albumName : nil
var albumName: String? = albumId != nil ? configuration.album?.albumName : nil
if albumId != nil {
// make sure the album exists on server, otherwise show error

View File

@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.135.3"
version_number: "1.135.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -10,5 +10,3 @@ enum TextSearchType {
}
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum SortUserBy { id }

View File

@@ -11,7 +11,6 @@ const Map<String, Locale> locales = {
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'),
'Chinese Traditional (zh_TW)':
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
'Croatian (hr)': Locale('hr'),
'Czech (cs)': Locale('cs'),
'Danish (da)': Locale('da'),
'Dutch (nl)': Locale('nl'),

View File

@@ -0,0 +1,10 @@
import 'dart:typed_data';
import 'dart:ui';
abstract interface class IAssetMediaRepository {
Future<Uint8List?> getThumbnail(
String id, {
int quality = 80,
Size size = const Size.square(256),
});
}

View File

@@ -0,0 +1,12 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/device_asset.model.dart';
abstract interface class IDeviceAssetRepository implements IDatabaseRepository {
Future<bool> updateAll(List<DeviceAsset> assetHash);
Future<List<DeviceAsset>> getByIds(List<String> localIds);
Future<void> deleteIds(List<String> ids);
}

View File

@@ -0,0 +1,14 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
abstract interface class IExifInfoRepository implements IDatabaseRepository {
Future<ExifInfo?> get(int assetId);
Future<ExifInfo> update(ExifInfo exifInfo);
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
Future<void> delete(int assetId);
Future<void> deleteAll();
}

View File

@@ -0,0 +1,33 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
Future<List<LocalAlbum>> getAll({Set<SortLocalAlbumsBy> sortBy = const {}});
Future<List<LocalAsset>> getAssets(String albumId);
Future<List<String>> getAssetIds(String albumId);
Future<void> upsert(
LocalAlbum album, {
Iterable<LocalAsset> toUpsert = const [],
Iterable<String> toDelete = const [],
});
Future<void> updateAll(Iterable<LocalAlbum> albums);
Future<void> delete(String albumId);
Future<void> processDelta({
required List<LocalAsset> updates,
required List<String> deletes,
required Map<String, List<String>> assetAlbums,
});
Future<void> syncDeletes(String albumId, Iterable<String> assetIdsToKeep);
Future<List<LocalAsset>> getAssetsToHash(String albumId);
}
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum }

View File

@@ -0,0 +1,6 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
abstract interface class ILocalAssetRepository implements IDatabaseRepository {
Future<void> updateHashes(Iterable<LocalAsset> hashes);
}

View File

@@ -0,0 +1,17 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
abstract interface class ILogRepository implements IDatabaseRepository {
Future<bool> insert(LogMessage log);
Future<bool> insertAll(Iterable<LogMessage> logs);
Future<List<LogMessage>> getAll();
Future<bool> deleteAll();
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
Future<void> truncate({int limit = 250});
}

View File

@@ -0,0 +1,7 @@
import 'dart:io';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
abstract interface class IStorageRepository {
Future<File?> getFileForAsset(LocalAsset asset);
}

View File

@@ -0,0 +1,20 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
abstract interface class IStoreRepository implements IDatabaseRepository {
Future<bool> insert<T>(StoreKey<T> key, T value);
Future<T?> tryGet<T>(StoreKey<T> key);
Future<List<StoreDto<Object>>> getAll();
Stream<T?> watch<T>(StoreKey<T> key);
Stream<StoreDto<Object>> watchAll();
Future<bool> update<T>(StoreKey<T> key, T value);
Future<void> delete<T>(StoreKey<T> key);
Future<void> deleteAll();
}

View File

@@ -0,0 +1,12 @@
import 'package:http/http.dart' as http;
import 'package:immich_mobile/domain/models/sync_event.model.dart';
abstract interface class ISyncApiRepository {
Future<void> ack(List<String> data);
Future<void> streamChanges(
Function(List<SyncEvent>, Function() abort) onData, {
int batchSize,
http.Client? httpClient,
});
}

View File

@@ -0,0 +1,18 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:openapi/api.dart';
abstract interface class ISyncStreamRepository implements IDatabaseRepository {
Future<void> updateUsersV1(Iterable<SyncUserV1> data);
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data);
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data);
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data);
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data);
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data);
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data);
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data);
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data);
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data);
}

View File

@@ -0,0 +1,27 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
abstract interface class ITimelineRepository implements IDatabaseRepository {
Stream<List<Bucket>> watchMainBucket(
List<String> timelineUsers, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
});
Future<List<BaseAsset>> getMainBucketAssets(
List<String> timelineUsers, {
required int offset,
required int count,
});
Stream<List<Bucket>> watchLocalBucket(
String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
});
Future<List<BaseAsset>> getLocalBucketAssets(
String albumId, {
required int offset,
required int count,
});
}

View File

@@ -0,0 +1,22 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserRepository implements IDatabaseRepository {
Future<bool> insert(UserDto user);
Future<UserDto?> getByUserId(String id);
Future<List<UserDto?>> getByUserIds(List<String> ids);
Future<List<UserDto>> getAll({SortUserBy? sortBy});
Future<bool> updateAll(List<UserDto> users);
Future<UserDto> update(UserDto user);
Future<void> delete(List<String> ids);
Future<void> deleteAll();
}
enum SortUserBy { id }

View File

@@ -0,0 +1,15 @@
import 'dart:typed_data';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserApiRepository {
Future<UserDto?> getMyUser();
Future<List<UserDto>> getAll();
/// Saves the [data] in the server and uses it as the current users profile image
Future<String> createProfileImage({
required String name,
required Uint8List data,
});
}

View File

@@ -1,10 +1,10 @@
import 'dart:convert';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:logging/logging.dart';
@@ -12,16 +12,16 @@ import 'package:logging/logging.dart';
class HashService {
final int batchSizeLimit;
final int batchFileLimit;
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftLocalAssetRepository _localAssetRepository;
final StorageRepository _storageRepository;
final ILocalAlbumRepository _localAlbumRepository;
final ILocalAssetRepository _localAssetRepository;
final IStorageRepository _storageRepository;
final NativeSyncApi _nativeSyncApi;
final _log = Logger('HashService');
HashService({
required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
required StorageRepository storageRepository,
required ILocalAlbumRepository localAlbumRepository,
required ILocalAssetRepository localAssetRepository,
required IStorageRepository storageRepository,
required NativeSyncApi nativeSyncApi,
this.batchSizeLimit = kBatchHashSizeLimit,
this.batchFileLimit = kBatchHashFileLimit,

View File

@@ -2,11 +2,11 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/utils/diff.dart';
@@ -14,14 +14,14 @@ import 'package:logging/logging.dart';
import 'package:platform/platform.dart';
class LocalSyncService {
final DriftLocalAlbumRepository _localAlbumRepository;
final ILocalAlbumRepository _localAlbumRepository;
final NativeSyncApi _nativeSyncApi;
final Platform _platform;
final StoreService _storeService;
final Logger _log = Logger("DeviceSyncService");
LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository,
required ILocalAlbumRepository localAlbumRepository,
required NativeSyncApi nativeSyncApi,
required StoreService storeService,
Platform? platform,

View File

@@ -2,10 +2,10 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:logging/logging.dart';
/// Service responsible for handling application logging.
@@ -14,8 +14,8 @@ import 'package:logging/logging.dart';
/// writes them to a persistent [ILogRepository], and manages log levels
/// via [IStoreRepository]
class LogService {
final IsarLogRepository _logRepository;
final IsarStoreRepository _storeRepository;
final ILogRepository _logRepository;
final IStoreRepository _storeRepository;
final List<LogMessage> _msgBuffer = [];
@@ -37,8 +37,8 @@ class LogService {
}
static Future<LogService> init({
required IsarLogRepository logRepository,
required IsarStoreRepository storeRepository,
required ILogRepository logRepository,
required IStoreRepository storeRepository,
bool shouldBuffer = true,
}) async {
_instance ??= await create(
@@ -50,8 +50,8 @@ class LogService {
}
static Future<LogService> create({
required IsarLogRepository logRepository,
required IsarStoreRepository storeRepository,
required ILogRepository logRepository,
required IStoreRepository storeRepository,
bool shouldBuffer = true,
}) async {
final instance = LogService._(logRepository, storeRepository, shouldBuffer);

View File

@@ -1,18 +1,18 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
/// Provides access to a persistent key-value store with an in-memory cache.
/// Listens for repository changes to keep the cache updated.
class StoreService {
final IsarStoreRepository _storeRepository;
final IStoreRepository _storeRepository;
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
late final StreamSubscription<StoreDto> _storeUpdateSubscription;
StoreService._({required IsarStoreRepository storeRepository})
StoreService._({required IStoreRepository storeRepository})
: _storeRepository = storeRepository;
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
@@ -26,14 +26,14 @@ class StoreService {
// TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({
required IsarStoreRepository storeRepository,
required IStoreRepository storeRepository,
}) async {
_instance ??= await create(storeRepository: storeRepository);
return _instance!;
}
static Future<StoreService> create({
required IsarStoreRepository storeRepository,
required IStoreRepository storeRepository,
}) async {
final instance = StoreService._(storeRepository: storeRepository);
await instance._populateCache();

View File

@@ -1,21 +1,21 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
class SyncStreamService {
final Logger _logger = Logger('SyncStreamService');
final SyncApiRepository _syncApiRepository;
final SyncStreamRepository _syncStreamRepository;
final ISyncApiRepository _syncApiRepository;
final ISyncStreamRepository _syncStreamRepository;
final bool Function()? _cancelChecker;
SyncStreamService({
required SyncApiRepository syncApiRepository,
required SyncStreamRepository syncStreamRepository,
required ISyncApiRepository syncApiRepository,
required ISyncStreamRepository syncStreamRepository,
bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository,

View File

@@ -3,11 +3,11 @@ import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/timeline.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
typedef TimelineAssetSource = Future<List<BaseAsset>> Function(
@@ -18,11 +18,11 @@ typedef TimelineAssetSource = Future<List<BaseAsset>> Function(
typedef TimelineBucketSource = Stream<List<Bucket>> Function();
class TimelineFactory {
final DriftTimelineRepository _timelineRepository;
final ITimelineRepository _timelineRepository;
final SettingsService _settingsService;
const TimelineFactory({
required DriftTimelineRepository timelineRepository,
required ITimelineRepository timelineRepository,
required SettingsService settingsService,
}) : _timelineRepository = timelineRepository,
_settingsService = settingsService;

View File

@@ -1,24 +1,24 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:logging/logging.dart';
class UserService {
final Logger _log = Logger("UserService");
final IsarUserRepository _isarUserRepository;
final UserApiRepository _userApiRepository;
final IUserRepository _userRepository;
final IUserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({
required IsarUserRepository isarUserRepository,
required UserApiRepository userApiRepository,
required IUserRepository userRepository,
required IUserApiRepository userApiRepository,
required StoreService storeService,
}) : _isarUserRepository = isarUserRepository,
}) : _userRepository = userRepository,
_userApiRepository = userApiRepository,
_storeService = storeService;
@@ -38,7 +38,7 @@ class UserService {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
await _storeService.put(StoreKey.currentUser, user);
await _isarUserRepository.update(user);
await _userRepository.update(user);
return user;
}
@@ -50,7 +50,7 @@ class UserService {
);
final updatedUser = getMyUser().copyWith(profileImagePath: path);
await _storeService.put(StoreKey.currentUser, updatedUser);
await _isarUserRepository.update(updatedUser);
await _userRepository.update(updatedUser);
return path;
} catch (e) {
_log.warning("Failed to upload profile image", e);
@@ -59,10 +59,10 @@ class UserService {
}
Future<List<UserDto>> getAll() async {
return await _isarUserRepository.getAll();
return await _userRepository.getAll();
}
Future<void> deleteAll() {
return _isarUserRepository.deleteAll();
return _userRepository.deleteAll();
}
}

View File

@@ -1,11 +1,13 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:immich_mobile/domain/interfaces/asset_media.interface.dart';
import 'package:photo_manager/photo_manager.dart';
class AssetMediaRepository {
class AssetMediaRepository implements IAssetMediaRepository {
const AssetMediaRepository();
@override
Future<Uint8List?> getThumbnail(
String id, {
int quality = 80,

View File

@@ -1,19 +1,23 @@
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
import 'package:immich_mobile/domain/models/device_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarDeviceAssetRepository extends IsarDatabaseRepository {
class IsarDeviceAssetRepository extends IsarDatabaseRepository
implements IDeviceAssetRepository {
final Isar _db;
const IsarDeviceAssetRepository(this._db) : super(_db);
@override
Future<void> deleteIds(List<String> ids) {
return transaction(() async {
await _db.deviceAssetEntitys.deleteAllByAssetId(ids.toList());
});
}
@override
Future<List<DeviceAsset>> getByIds(List<String> localIds) {
return _db.deviceAssetEntitys
.where()
@@ -22,6 +26,7 @@ class IsarDeviceAssetRepository extends IsarDatabaseRepository {
.then((value) => value.map((e) => e.toModel()).toList());
}
@override
Future<bool> updateAll(List<DeviceAsset> assetHash) {
return transaction(() async {
await _db.deviceAssetEntitys

View File

@@ -1,30 +1,36 @@
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarExifRepository extends IsarDatabaseRepository {
class IsarExifRepository extends IsarDatabaseRepository
implements IExifInfoRepository {
final Isar _db;
const IsarExifRepository(this._db) : super(_db);
@override
Future<void> delete(int assetId) async {
await transaction(() async {
await _db.exifInfos.delete(assetId);
});
}
@override
Future<void> deleteAll() async {
await transaction(() async {
await _db.exifInfos.clear();
});
}
@override
Future<ExifInfo?> get(int assetId) async {
return (await _db.exifInfos.get(assetId))?.toDto();
}
@override
Future<ExifInfo> update(ExifInfo exifInfo) {
return transaction(() async {
await _db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo));
@@ -32,6 +38,7 @@ class IsarExifRepository extends IsarDatabaseRepository {
});
}
@override
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos) {
return transaction(() async {
await _db.exifInfos.putAll(

View File

@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
@@ -7,15 +8,15 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.d
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:platform/platform.dart';
enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum }
class DriftLocalAlbumRepository extends DriftDatabaseRepository {
class DriftLocalAlbumRepository extends DriftDatabaseRepository
implements ILocalAlbumRepository {
final Drift _db;
final Platform _platform;
const DriftLocalAlbumRepository(this._db, {Platform? platform})
: _platform = platform ?? const LocalPlatform(),
super(_db);
@override
Future<List<LocalAlbum>> getAll({Set<SortLocalAlbumsBy> sortBy = const {}}) {
final assetCount = _db.localAlbumAssetEntity.assetId.count();
@@ -55,6 +56,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
.get();
}
@override
Future<void> delete(String albumId) => transaction(() async {
// Remove all assets that are only in this particular album
// We cannot remove all assets in the album because they might be in other albums in iOS
@@ -70,6 +72,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
.delete();
});
@override
Future<void> syncDeletes(
String albumId,
Iterable<String> assetIdsToKeep,
@@ -98,6 +101,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
await deleteSmt.go();
}
@override
Future<void> upsert(
LocalAlbum localAlbum, {
Iterable<LocalAsset> toUpsert = const [],
@@ -130,6 +134,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
});
}
@override
Future<void> updateAll(Iterable<LocalAlbum> albums) {
return _db.transaction(() async {
await _db.localAlbumEntity
@@ -180,6 +185,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
});
}
@override
Future<List<LocalAsset>> getAssets(String albumId) {
final query = _db.localAlbumAssetEntity.select().join(
[
@@ -196,6 +202,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
.get();
}
@override
Future<List<String>> getAssetIds(String albumId) {
final query = _db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
@@ -205,6 +212,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
.get();
}
@override
Future<void> processDelta({
required List<LocalAsset> updates,
required List<String> deletes,
@@ -245,6 +253,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
});
}
@override
Future<List<LocalAsset>> getAssetsToHash(String albumId) {
final query = _db.localAlbumAssetEntity.select().join(
[

View File

@@ -1,12 +1,15 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftLocalAssetRepository extends DriftDatabaseRepository {
class DriftLocalAssetRepository extends DriftDatabaseRepository
implements ILocalAssetRepository {
final Drift _db;
const DriftLocalAssetRepository(this._db) : super(_db);
@override
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
if (hashes.isEmpty) {
return Future.value();

View File

@@ -1,23 +1,28 @@
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarLogRepository extends IsarDatabaseRepository {
class IsarLogRepository extends IsarDatabaseRepository
implements ILogRepository {
final Isar _db;
const IsarLogRepository(super.db) : _db = db;
@override
Future<bool> deleteAll() async {
await transaction(() async => await _db.loggerMessages.clear());
return true;
}
@override
Future<List<LogMessage>> getAll() async {
final logs =
await _db.loggerMessages.where().sortByCreatedAtDesc().findAll();
return logs.map((l) => l.toDto()).toList();
}
@override
Future<bool> insert(LogMessage log) async {
final logEntity = LoggerMessage.fromDto(log);
await transaction(() async {
@@ -26,6 +31,7 @@ class IsarLogRepository extends IsarDatabaseRepository {
return true;
}
@override
Future<bool> insertAll(Iterable<LogMessage> logs) async {
await transaction(() async {
final logEntities =
@@ -35,6 +41,7 @@ class IsarLogRepository extends IsarDatabaseRepository {
return true;
}
@override
Future<void> truncate({int limit = 250}) async {
await transaction(() async {
final count = await _db.loggerMessages.count();

View File

@@ -1,12 +1,14 @@
import 'dart:io';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
class StorageRepository {
class StorageRepository implements IStorageRepository {
final _log = Logger('StorageRepository');
@override
Future<File?> getFileForAsset(LocalAsset asset) async {
File? file;
try {

View File

@@ -1,3 +1,4 @@
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
@@ -5,12 +6,14 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:isar/isar.dart';
class IsarStoreRepository extends IsarDatabaseRepository {
class IsarStoreRepository extends IsarDatabaseRepository
implements IStoreRepository {
final Isar _db;
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
IsarStoreRepository(super.db) : _db = db;
@override
Future<bool> deleteAll() async {
return await transaction(() async {
await _db.storeValues.clear();
@@ -18,6 +21,7 @@ class IsarStoreRepository extends IsarDatabaseRepository {
});
}
@override
Stream<StoreDto<Object>> watchAll() {
return _db.storeValues
.filter()
@@ -30,10 +34,12 @@ class IsarStoreRepository extends IsarDatabaseRepository {
);
}
@override
Future<void> delete<T>(StoreKey<T> key) async {
return await transaction(() async => await _db.storeValues.delete(key.id));
}
@override
Future<bool> insert<T>(StoreKey<T> key, T value) async {
return await transaction(() async {
await _db.storeValues.put(await _fromValue(key, value));
@@ -41,6 +47,7 @@ class IsarStoreRepository extends IsarDatabaseRepository {
});
}
@override
Future<T?> tryGet<T>(StoreKey<T> key) async {
final entity = (await _db.storeValues.get(key.id));
if (entity == null) {
@@ -49,6 +56,7 @@ class IsarStoreRepository extends IsarDatabaseRepository {
return await _toValue(key, entity);
}
@override
Future<bool> update<T>(StoreKey<T> key, T value) async {
return await transaction(() async {
await _db.storeValues.put(await _fromValue(key, value));
@@ -56,6 +64,7 @@ class IsarStoreRepository extends IsarDatabaseRepository {
});
}
@override
Stream<T?> watch<T>(StoreKey<T> key) async* {
yield* _db.storeValues
.watchObject(key.id, fireImmediately: true)
@@ -100,6 +109,7 @@ class IsarStoreRepository extends IsarDatabaseRepository {
return StoreValue(key.id, intValue: intValue, strValue: strValue);
}
@override
Future<List<StoreDto<Object>>> getAll() async {
final entities = await _db.storeValues
.filter()

View File

@@ -3,21 +3,24 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
class SyncApiRepository {
class SyncApiRepository implements ISyncApiRepository {
final Logger _logger = Logger('SyncApiRepository');
final ApiService _api;
SyncApiRepository(this._api);
@override
Future<void> ack(List<String> data) {
return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data));
}
@override
Future<void> streamChanges(
Function(List<SyncEvent>, Function() abort) onData, {
int batchSize = kSyncEventBatchSize,

View File

@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
@@ -9,12 +10,14 @@ import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class SyncStreamRepository extends DriftDatabaseRepository {
class DriftSyncStreamRepository extends DriftDatabaseRepository
implements ISyncStreamRepository {
final Logger _logger = Logger('DriftSyncStreamRepository');
final Drift _db;
SyncStreamRepository(super.db) : _db = db;
DriftSyncStreamRepository(super.db) : _db = db;
@override
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -31,6 +34,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateUsersV1(Iterable<SyncUserV1> data) async {
try {
await _db.batch((batch) {
@@ -53,6 +57,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -72,6 +77,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data) async {
try {
await _db.batch((batch) {
@@ -95,6 +101,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
try {
await _deleteAssetsV1(data);
@@ -104,6 +111,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
@@ -113,6 +121,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
try {
await _deleteAssetsV1(data);
@@ -122,6 +131,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
@@ -131,6 +141,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
try {
await _updateAssetExifV1(data);
@@ -140,6 +151,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
try {
await _updateAssetExifV1(data);

View File

@@ -3,13 +3,15 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/timeline.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:stream_transform/stream_transform.dart';
class DriftTimelineRepository extends DriftDatabaseRepository {
class DriftTimelineRepository extends DriftDatabaseRepository
implements ITimelineRepository {
final Drift _db;
const DriftTimelineRepository(super._db) : _db = _db;
@@ -26,6 +28,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return buckets;
}
@override
Stream<List<Bucket>> watchMainBucket(
List<String> userIds, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
@@ -46,6 +49,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.throttle(const Duration(seconds: 3), trailing: true);
}
@override
Future<List<BaseAsset>> getMainBucketAssets(
List<String> userIds, {
required int offset,
@@ -86,6 +90,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.get();
}
@override
Stream<List<Bucket>> watchLocalBucket(
String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
@@ -119,6 +124,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch();
}
@override
Future<List<BaseAsset>> getLocalBucketAssets(
String albumId, {
required int offset,

View File

@@ -1,26 +1,30 @@
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarUserRepository extends IsarDatabaseRepository {
class IsarUserRepository extends IsarDatabaseRepository
implements IUserRepository {
final Isar _db;
const IsarUserRepository(super.db) : _db = db;
@override
Future<void> delete(List<String> ids) async {
await transaction(() async {
await _db.users.deleteAllById(ids);
});
}
@override
Future<void> deleteAll() async {
await transaction(() async {
await _db.users.clear();
});
}
@override
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
return (await _db.users
.where()
@@ -35,14 +39,17 @@ class IsarUserRepository extends IsarDatabaseRepository {
.toList();
}
@override
Future<UserDto?> getByUserId(String id) async {
return (await _db.users.getById(id))?.toDto();
}
@override
Future<List<UserDto?>> getByUserIds(List<String> ids) async {
return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList();
}
@override
Future<bool> insert(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
@@ -50,6 +57,7 @@ class IsarUserRepository extends IsarDatabaseRepository {
return true;
}
@override
Future<UserDto> update(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
@@ -57,6 +65,7 @@ class IsarUserRepository extends IsarDatabaseRepository {
return user;
}
@override
Future<bool> updateAll(List<UserDto> users) async {
await transaction(() async {
await _db.users.putAll(users.map(entity.User.fromDto).toList());

View File

@@ -1,15 +1,17 @@
import 'dart:typed_data';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:openapi/api.dart';
class UserApiRepository extends ApiRepository {
class UserApiRepository extends ApiRepository implements IUserApiRepository {
final UsersApi _api;
const UserApiRepository(this._api);
@override
Future<UserDto?> getMyUser() async {
final (adminDto, preferenceDto) =
await (_api.getMyUser(), _api.getMyPreferences()).wait;
@@ -18,6 +20,7 @@ class UserApiRepository extends ApiRepository {
return UserConverter.fromAdminDto(adminDto, preferenceDto);
}
@override
Future<String> createProfileImage({
required String name,
required Uint8List data,
@@ -30,6 +33,7 @@ class UserApiRepository extends ApiRepository {
return res.profileImagePath;
}
@override
Future<List<UserDto>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(UserConverter.fromSimpleUserDto).toList();

View File

@@ -0,0 +1,16 @@
import 'package:immich_mobile/models/activities/activity.model.dart';
abstract interface class IActivityApiRepository {
Future<List<Activity>> getAll(
String albumId, {
String? assetId,
});
Future<Activity> create(
String albumId,
ActivityType type, {
String? assetId,
String? comment,
});
Future<void> delete(String id);
Future<ActivityStats> getStats(String albumId, {String? assetId});
}

View File

@@ -0,0 +1,55 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
abstract interface class IAlbumRepository implements IDatabaseRepository {
Future<Album> create(Album album);
Future<Album?> get(int id);
Future<Album?> getByName(
String name, {
bool? shared,
bool? remote,
bool? owner,
});
Future<List<Album>> getAll({
bool? shared,
bool? remote,
int? ownerId,
AlbumSort? sortBy,
});
Future<Album> update(Album album);
Future<void> delete(int albumId);
Future<void> deleteAllLocal();
Future<int> count({bool? local});
Future<void> addUsers(Album album, List<UserDto> users);
Future<void> removeUsers(Album album, List<UserDto> users);
Future<void> addAssets(Album album, List<Asset> assets);
Future<void> removeAssets(Album album, List<Asset> assets);
Future<Album> recalculateMetadata(Album album);
Future<List<Album>> search(String searchTerm, QuickFilterMode filterMode);
Stream<List<Album>> watchRemoteAlbums();
Stream<List<Album>> watchLocalAlbums();
Stream<Album?> watchAlbum(int id);
Future<void> clearTable();
}
enum AlbumSort { remoteId, localId }

View File

@@ -0,0 +1,42 @@
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
abstract interface class IAlbumApiRepository {
Future<Album> get(String id);
Future<List<Album>> getAll({bool? shared});
Future<Album> create(
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
});
Future<Album> update(
String albumId, {
String? name,
String? thumbnailAssetId,
String? description,
bool? activityEnabled,
SortOrder? sortOrder,
});
Future<void> delete(String albumId);
Future<({List<String> added, List<String> duplicates})> addAssets(
String albumId,
Iterable<String> assetIds,
);
Future<({List<String> removed, List<String> failed})> removeAssets(
String albumId,
Iterable<String> assetIds,
);
Future<Album> addUsers(
String albumId,
Iterable<String> userIds,
);
Future<void> removeUser(String albumId, {required String userId});
}

View File

@@ -0,0 +1,21 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IAlbumMediaRepository {
Future<List<Album>> getAll();
Future<List<String>> getAssetIds(String albumId);
Future<int> getAssetCount(String albumId);
Future<List<Asset>> getAssets(
String albumId, {
int start = 0,
int end = 0x7fffffffffffffff,
DateTime? modifiedFrom,
DateTime? modifiedUntil,
bool orderByModificationDate = false,
});
Future<Album> get(String id);
}

View File

@@ -0,0 +1,68 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IAssetRepository implements IDatabaseRepository {
Future<Asset?> getByRemoteId(String id);
Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum);
Future<List<Asset>> getAllByRemoteId(
Iterable<String> ids, {
AssetState? state,
});
Future<List<Asset?>> getAllByOwnerIdChecksum(
List<int> ids,
List<String> checksums,
);
Future<List<Asset>> getAll({
required String ownerId,
AssetState? state,
AssetSort? sortBy,
int? limit,
});
Future<List<Asset>> getAllLocal();
Future<List<Asset>> getByAlbum(
Album album, {
Iterable<String> notOwnedBy = const [],
String? ownerId,
AssetState? state,
AssetSort? sortBy,
});
Future<Asset> update(Asset asset);
Future<List<Asset>> updateAll(List<Asset> assets);
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
Future<void> deleteByIds(List<int> ids);
Future<List<Asset>> getMatches({
required List<Asset> assets,
required String ownerId,
AssetState? state,
int limit = 100,
});
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
Future<List<String>> getAllDuplicatedAssetIds();
Future<List<Asset>> getStackAssets(String stackId);
Future<void> clearTable();
Stream<Asset?> watchAsset(int id, {bool fireImmediately = false});
Future<List<Asset>> getTrashAssets(String userId);
Future<List<Asset>> getRecentlyTakenAssets(String userId);
Future<List<Asset>> getMotionAssets(String userId);
}
enum AssetSort { checksum, ownerIdChecksum }

View File

@@ -0,0 +1,26 @@
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IAssetApiRepository {
// Future<Asset> get(String id);
// Future<List<Asset>> getAll();
// Future<Asset> create(Asset asset);
Future<Asset> update(
String id, {
String? description,
});
// Future<void> delete(String id);
Future<List<Asset>> search({List<String> personIds = const []});
Future<void> updateVisibility(
List<String> list,
AssetVisibilityEnum visibility,
);
Future<String?> getAssetMIMEType(String id);
}

View File

@@ -0,0 +1,10 @@
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IAssetMediaRepository {
Future<List<String>> deleteAll(List<String> ids);
Future<Asset?> get(String id);
/// Obtaining the correct original filename of the asset
Future<String?> getOriginalFilename(String id);
}

View File

@@ -0,0 +1,11 @@
import 'package:immich_mobile/interfaces/database.interface.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
abstract interface class IAuthRepository implements IDatabaseRepository {
Future<void> clearLocalData();
String getAccessToken();
bool getEndpointSwitchingFeature();
String? getPreferredWifiName();
String? getLocalEndpoint();
List<AuxilaryEndpoint> getExternalEndpointList();
}

View File

@@ -0,0 +1,14 @@
import 'package:immich_mobile/models/auth/login_response.model.dart';
abstract interface class IAuthApiRepository {
Future<LoginResponse> login(String email, String password);
Future<void> logout();
Future<void> changePassword(String newPassword);
Future<bool> unlockPinCode(String pinCode);
Future<void> lockPinCode();
Future<void> setupPinCode(String pinCode);
}

View File

@@ -0,0 +1,16 @@
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IBackupAlbumRepository implements IDatabaseRepository {
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort});
Future<List<String>> getIdsBySelection(BackupSelection backup);
Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup);
Future<void> updateAll(List<BackupAlbum> backupAlbums);
Future<void> deleteAll(List<int> ids);
}
enum BackupAlbumSort { id }

View File

@@ -0,0 +1,6 @@
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
abstract interface class IBiometricRepository {
Future<BiometricStatus> getStatus();
Future<bool> authenticate(String? message);
}

View File

@@ -0,0 +1,27 @@
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/models/cast/cast_manager_state.dart';
abstract interface class ICastDestinationService {
Future<bool> initialize();
CastDestinationType getType();
void Function(bool)? onConnectionState;
void Function(Duration)? onCurrentTime;
void Function(Duration)? onDuration;
void Function(String)? onReceiverName;
void Function(CastState)? onCastState;
Future<void> connect(dynamic device);
void loadMedia(Asset asset, bool reload);
void play();
void pause();
void seekTo(Duration position);
void stop();
Future<void> disconnect();
Future<List<(String, CastDestinationType, dynamic)>> getDevices();
}

View File

@@ -0,0 +1,15 @@
import 'package:background_downloader/background_downloader.dart';
abstract interface class IDownloadRepository {
void Function(TaskStatusUpdate)? onImageDownloadStatus;
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
void Function(TaskProgressUpdate)? onTaskProgress;
Future<List<TaskRecord>> getLiveVideoTasks();
Future<List<bool>> downloadAll(List<DownloadTask> tasks);
Future<bool> cancel(String id);
Future<void> deleteAllTrackingRecords();
Future<void> deleteRecordsWithIds(List<String> id);
}

View File

@@ -0,0 +1,16 @@
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IETagRepository implements IDatabaseRepository {
Future<ETag?> get(String id);
Future<ETag?> getById(String id);
Future<List<String>> getAllIds();
Future<void> upsertAll(List<ETag> etags);
Future<void> deleteByIds(List<String> ids);
Future<void> clearTable();
}

View File

@@ -0,0 +1,36 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IFileMediaRepository {
Future<Asset?> saveImage(
Uint8List data, {
required String title,
String? relativePath,
});
Future<Asset?> saveImageWithFile(
String filePath, {
String? title,
String? relativePath,
});
Future<Asset?> saveVideo(
File file, {
required String title,
String? relativePath,
});
Future<Asset?> saveLivePhoto({
required File image,
required File video,
required String title,
});
Future<void> clearFileCache();
Future<void> enableBackgroundAccess();
Future<void> requestExtendedPermissions();
}

View File

@@ -0,0 +1,6 @@
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IFolderApiRepository {
Future<List<String>> getAllUniquePaths();
Future<List<Asset>> getAssetsForPath(String? path);
}

View File

@@ -0,0 +1,5 @@
abstract interface class ILocalFilesManager {
Future<bool> moveToTrash(List<String> mediaUrls);
Future<bool> restoreFromTrash(String fileName, int type);
Future<bool> requestManageMediaPermission();
}

View File

@@ -0,0 +1,4 @@
abstract interface class INetworkRepository {
Future<String?> getWifiName();
Future<String?> getWifiIp();
}

View File

@@ -0,0 +1,8 @@
import 'package:immich_mobile/domain/models/user.model.dart';
abstract class IPartnerRepository {
Future<List<UserDto>> getSharedWith();
Future<List<UserDto>> getSharedBy();
Stream<List<UserDto>> watchSharedWith();
Stream<List<UserDto>> watchSharedBy();
}

View File

@@ -0,0 +1,13 @@
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IPartnerApiRepository {
Future<List<UserDto>> getAll(Direction direction);
Future<UserDto> create(String id);
Future<UserDto> update(String id, {required bool inTimeline});
Future<void> delete(String id);
}
enum Direction {
sharedWithMe,
sharedByMe,
}

View File

@@ -1,5 +1,11 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
abstract interface class IPersonApiRepository {
Future<List<Person>> getAll();
Future<Person> update(String id, {String? name});
}
class Person {
Person({
required this.id,

View File

@@ -0,0 +1,5 @@
abstract interface class ISecureStorageRepository {
Future<String?> read(String key);
Future<void> write(String key, String value);
Future<void> delete(String key);
}

View File

@@ -0,0 +1,9 @@
import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
abstract interface class ISessionAPIRepository {
Future<SessionCreateResponse> createSession(
String deviceName,
String deviceOS, {
int? duration,
});
}

View File

@@ -0,0 +1,7 @@
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
abstract interface class IShareHandlerRepository {
void Function(List<ShareIntentAttachment>)? onSharedMedia;
Future<void> init();
}

View File

@@ -0,0 +1,39 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
abstract class ITimelineRepository {
Future<List<String>> getTimelineUserIds(String id);
Stream<List<String>> watchTimelineUsers(String id);
Stream<RenderList> watchArchiveTimeline(String userId);
Stream<RenderList> watchFavoriteTimeline(String userId);
Stream<RenderList> watchTrashTimeline(String userId);
Stream<RenderList> watchAlbumTimeline(
Album album,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchAllVideosTimeline(String userId);
Stream<RenderList> watchHomeTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchMultiUsersTimeline(
List<String> userIds,
GroupAssetsBy groupAssetsBy,
);
Future<RenderList> getTimelineFromAssets(
List<Asset> assets,
GroupAssetsBy getGroupByOption,
);
Stream<RenderList> watchAssetSelectionTimeline(String userId);
Stream<RenderList> watchLockedTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
}

View File

@@ -0,0 +1,11 @@
import 'package:background_downloader/background_downloader.dart';
abstract interface class IUploadRepository {
void Function(TaskStatusUpdate)? onUploadStatus;
void Function(TaskProgressUpdate)? onTaskProgress;
Future<bool> upload(UploadTask task);
Future<bool> cancel(String id);
Future<void> deleteAllTrackingRecords();
Future<void> deleteRecordsWithIds(List<String> id);
}

View File

@@ -0,0 +1,5 @@
abstract interface class IWidgetRepository {
Future<void> saveData(String key, String value);
Future<void> refresh(String name);
Future<void> setAppGroupId(String appGroupId);
}

View File

@@ -1,8 +1,8 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
class SearchLocationFilter {
String? country;

View File

@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';

View File

@@ -4,13 +4,14 @@ import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:immich_mobile/domain/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
final AssetMediaRepository _assetMediaRepository =
final IAssetMediaRepository _assetMediaRepository =
const AssetMediaRepository();
final CacheManager? cacheManager;

View File

@@ -118,8 +118,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
}) async {
await _apiService.setAccessToken(accessToken);
await _widgetService.writeSessionKey(accessToken);
await _widgetService.writeServerList();
await _widgetService.writeCredentials(
Store.get(StoreKey.serverEndpoint),
accessToken,
);
// Get the deviceid from the store if it exists, otherwise generate a new one
String deviceId =
@@ -188,7 +190,6 @@ class AuthNotifier extends StateNotifier<AuthState> {
Future<void> saveLocalEndpoint(String url) async {
await Store.put(StoreKey.localEndpoint, url);
await _widgetService.writeServerList();
}
String? getSavedWifiName() {

View File

@@ -9,6 +9,9 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
@@ -22,7 +25,6 @@ import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/services/backup.service.dart';
@@ -106,8 +108,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final AuthState _authState;
final BackgroundService _backgroundService;
final GalleryPermissionNotifier _galleryPermissionNotifier;
final AlbumMediaRepository _albumMediaRepository;
final FileMediaRepository _fileMediaRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository;
final BackupAlbumService _backupAlbumService;
final Ref ref;

View File

@@ -1,5 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/cast_destination_service.interface.dart';
import 'package:immich_mobile/models/cast/cast_manager_state.dart';
import 'package:immich_mobile/services/gcast.service.dart';
@@ -9,7 +10,7 @@ final castProvider = StateNotifierProvider<CastNotifier, CastManagerState>(
class CastNotifier extends StateNotifier<CastManagerState> {
// more cast providers can be added here (ie Fcast)
final GCastService _gCastService;
final ICastDestinationService _gCastService;
List<(String, CastDestinationType, dynamic)> discovered = List.empty();

View File

@@ -1,7 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
final localAlbumRepository = Provider<ILocalAlbumRepository>(
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
);

View File

@@ -1,7 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final localAssetRepository = Provider<DriftLocalAssetRepository>(
final localAssetRepository = Provider<ILocalAssetRepository>(
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
);

View File

@@ -1,7 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/device_asset.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final deviceAssetRepositoryProvider = Provider<IsarDeviceAssetRepository>(
final deviceAssetRepositoryProvider = Provider<IDeviceAssetRepository>(
(ref) => IsarDeviceAssetRepository(ref.watch(isarProvider)),
);

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -6,5 +7,5 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'exif.provider.g.dart';
@Riverpod(keepAlive: true)
IsarExifRepository exifRepository(Ref ref) =>
IExifInfoRepository exifRepository(Ref ref) =>
IsarExifRepository(ref.watch(isarProvider));

View File

@@ -6,11 +6,11 @@ part of 'exif.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$exifRepositoryHash() => r'bf4a3f6a50d954a23d317659b4f3e2f381066463';
String _$exifRepositoryHash() => r'f0abe778ed61fbb257001fdf2ac6e17814011fee';
/// See also [exifRepository].
@ProviderFor(exifRepository)
final exifRepositoryProvider = Provider<IsarExifRepository>.internal(
final exifRepositoryProvider = Provider<IExifInfoRepository>.internal(
exifRepository,
name: r'exifRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,6 +22,6 @@ final exifRepositoryProvider = Provider<IsarExifRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ExifRepositoryRef = ProviderRef<IsarExifRepository>;
typedef ExifRepositoryRef = ProviderRef<IExifInfoRepository>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -1,6 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
final storageRepositoryProvider = Provider<StorageRepository>(
final storageRepositoryProvider = Provider<IStorageRepository>(
(ref) => StorageRepository(),
);

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@@ -7,7 +8,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'store.provider.g.dart';
@Riverpod(keepAlive: true)
IsarStoreRepository storeRepository(Ref ref) =>
IStoreRepository storeRepository(Ref ref) =>
IsarStoreRepository(ref.watch(isarProvider));
@Riverpod(keepAlive: true)

View File

@@ -6,11 +6,11 @@ part of 'store.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$storeRepositoryHash() => r'659cb134466e4b0d5f04e2fc93e426350d99545f';
String _$storeRepositoryHash() => r'99d24875d30c5e86b1c6caa352a0026167114e62';
/// See also [storeRepository].
@ProviderFor(storeRepository)
final storeRepositoryProvider = Provider<IsarStoreRepository>.internal(
final storeRepositoryProvider = Provider<IStoreRepository>.internal(
storeRepository,
name: r'storeRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,7 +22,7 @@ final storeRepositoryProvider = Provider<IsarStoreRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef StoreRepositoryRef = ProviderRef<IsarStoreRepository>;
typedef StoreRepositoryRef = ProviderRef<IStoreRepository>;
String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0';
/// See also [storeService].

View File

@@ -26,7 +26,7 @@ final syncApiRepositoryProvider = Provider(
);
final syncStreamRepositoryProvider = Provider(
(ref) => SyncStreamRepository(ref.watch(driftProvider)),
(ref) => DriftSyncStreamRepository(ref.watch(driftProvider)),
);
final localSyncServiceProvider = Provider(

View File

@@ -1,11 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/timeline.interface.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
final timelineRepositoryProvider = Provider<DriftTimelineRepository>(
final timelineRepositoryProvider = Provider<ITimelineRepository>(
(ref) => DriftTimelineRepository(ref.watch(driftProvider)),
);

View File

@@ -1,4 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
@@ -10,16 +12,16 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user.provider.g.dart';
@Riverpod(keepAlive: true)
IsarUserRepository userRepository(Ref ref) =>
IUserRepository userRepository(Ref ref) =>
IsarUserRepository(ref.watch(isarProvider));
@Riverpod(keepAlive: true)
UserApiRepository userApiRepository(Ref ref) =>
IUserApiRepository userApiRepository(Ref ref) =>
UserApiRepository(ref.watch(apiServiceProvider).usersApi);
@Riverpod(keepAlive: true)
UserService userService(Ref ref) => UserService(
isarUserRepository: ref.watch(userRepositoryProvider),
userRepository: ref.watch(userRepositoryProvider),
userApiRepository: ref.watch(userApiRepositoryProvider),
storeService: ref.watch(storeServiceProvider),
);

View File

@@ -6,11 +6,11 @@ part of 'user.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3';
String _$userRepositoryHash() => r'1a2ac726bcc44397dcaecf449084fefd336696d4';
/// See also [userRepository].
@ProviderFor(userRepository)
final userRepositoryProvider = Provider<IsarUserRepository>.internal(
final userRepositoryProvider = Provider<IUserRepository>.internal(
userRepository,
name: r'userRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,12 +22,12 @@ final userRepositoryProvider = Provider<IsarUserRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserRepositoryRef = ProviderRef<IsarUserRepository>;
String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2';
typedef UserRepositoryRef = ProviderRef<IUserRepository>;
String _$userApiRepositoryHash() => r'6b19f2c99fb83162a5ceb91adb8589eaae01bc92';
/// See also [userApiRepository].
@ProviderFor(userApiRepository)
final userApiRepositoryProvider = Provider<UserApiRepository>.internal(
final userApiRepositoryProvider = Provider<IUserApiRepository>.internal(
userApiRepository,
name: r'userApiRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -39,8 +39,8 @@ final userApiRepositoryProvider = Provider<UserApiRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserApiRepositoryRef = ProviderRef<UserApiRepository>;
String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1';
typedef UserApiRepositoryRef = ProviderRef<IUserApiRepository>;
String _$userServiceHash() => r'4a0873357b7115b4d6bfa8e89b847c0b74ce0d93';
/// See also [userService].
@ProviderFor(userService)

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