Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1183f4b4b | ||
|
|
84cfa38510 | ||
|
|
89edbcacfa |
@@ -5,7 +5,7 @@ import uvicorn
|
||||
|
||||
from insightface.app import FaceAnalysis
|
||||
from transformers import pipeline
|
||||
from sentence_transformers import SentenceTransformer, util
|
||||
from sentence_transformers import SentenceTransformer
|
||||
from PIL import Image
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
@@ -20,22 +20,32 @@ class ClipRequestBody(BaseModel):
|
||||
|
||||
|
||||
classification_model = os.getenv(
|
||||
'MACHINE_LEARNING_CLASSIFICATION_MODEL', 'microsoft/resnet-50')
|
||||
object_model = os.getenv('MACHINE_LEARNING_OBJECT_MODEL', 'hustvl/yolos-tiny')
|
||||
clip_image_model = os.getenv(
|
||||
'MACHINE_LEARNING_CLIP_IMAGE_MODEL', 'clip-ViT-B-32')
|
||||
clip_text_model = os.getenv(
|
||||
'MACHINE_LEARNING_CLIP_TEXT_MODEL', 'clip-ViT-B-32')
|
||||
"MACHINE_LEARNING_CLASSIFICATION_MODEL", "microsoft/resnet-50"
|
||||
)
|
||||
object_model = os.getenv("MACHINE_LEARNING_OBJECT_MODEL", "hustvl/yolos-tiny")
|
||||
clip_image_model = os.getenv("MACHINE_LEARNING_CLIP_IMAGE_MODEL", "clip-ViT-B-32")
|
||||
clip_text_model = os.getenv("MACHINE_LEARNING_CLIP_TEXT_MODEL", "clip-ViT-B-32")
|
||||
facial_recognition_model = os.getenv(
|
||||
'MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL', 'buffalo_l')
|
||||
"MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL", "buffalo_l"
|
||||
)
|
||||
|
||||
cache_folder = os.getenv('MACHINE_LEARNING_CACHE_FOLDER', '/cache')
|
||||
cache_folder = os.getenv("MACHINE_LEARNING_CACHE_FOLDER", "/cache")
|
||||
|
||||
_model_cache = {}
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
# Get all models
|
||||
_get_model(object_model, "object-detection")
|
||||
_get_model(classification_model, "image-classification")
|
||||
_get_model(clip_image_model)
|
||||
_get_model(clip_text_model)
|
||||
_get_model(facial_recognition_model, "facial-recognition")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Immich ML"}
|
||||
@@ -48,14 +58,14 @@ def ping():
|
||||
|
||||
@app.post("/object-detection/detect-object", status_code=200)
|
||||
def object_detection(payload: MlRequestBody):
|
||||
model = _get_model(object_model, 'object-detection')
|
||||
model = _get_model(object_model, "object-detection")
|
||||
assetPath = payload.thumbnailPath
|
||||
return run_engine(model, assetPath)
|
||||
|
||||
|
||||
@app.post("/image-classifier/tag-image", status_code=200)
|
||||
def image_classification(payload: MlRequestBody):
|
||||
model = _get_model(classification_model, 'image-classification')
|
||||
model = _get_model(classification_model, "image-classification")
|
||||
assetPath = payload.thumbnailPath
|
||||
return run_engine(model, assetPath)
|
||||
|
||||
@@ -76,31 +86,32 @@ def clip_encode_text(payload: ClipRequestBody):
|
||||
|
||||
@app.post("/facial-recognition/detect-faces", status_code=200)
|
||||
def facial_recognition(payload: MlRequestBody):
|
||||
model = _get_model(facial_recognition_model, 'facial-recognition')
|
||||
model = _get_model(facial_recognition_model, "facial-recognition")
|
||||
assetPath = payload.thumbnailPath
|
||||
img = cv.imread(assetPath)
|
||||
height, width, _ = img.shape
|
||||
results = []
|
||||
faces = model.get(img)
|
||||
|
||||
for face in faces:
|
||||
if face.det_score < 0.7:
|
||||
continue
|
||||
x1, y1, x2, y2 = face.bbox
|
||||
# min face size as percent of original image
|
||||
# if (x2 - x1) / width < 0.03 or (y2 - y1) / height < 0.05:
|
||||
# continue
|
||||
results.append({
|
||||
"imageWidth": width,
|
||||
"imageHeight": height,
|
||||
"boundingBox": {
|
||||
"x1": round(x1),
|
||||
"y1": round(y1),
|
||||
"x2": round(x2),
|
||||
"y2": round(y2),
|
||||
},
|
||||
"score": face.det_score.item(),
|
||||
"embedding": face.normed_embedding.tolist()
|
||||
})
|
||||
|
||||
results.append(
|
||||
{
|
||||
"imageWidth": width,
|
||||
"imageHeight": height,
|
||||
"boundingBox": {
|
||||
"x1": round(x1),
|
||||
"y1": round(y1),
|
||||
"x2": round(x2),
|
||||
"y2": round(y2),
|
||||
},
|
||||
"score": face.det_score.item(),
|
||||
"embedding": face.normed_embedding.tolist(),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@@ -109,11 +120,11 @@ def run_engine(engine, path):
|
||||
predictions = engine(path)
|
||||
|
||||
for index, pred in enumerate(predictions):
|
||||
tags = pred['label'].split(', ')
|
||||
if (pred['score'] > 0.9):
|
||||
tags = pred["label"].split(", ")
|
||||
if pred["score"] > 0.9:
|
||||
result = [*result, *tags]
|
||||
|
||||
if (len(result) > 1):
|
||||
if len(result) > 1:
|
||||
result = list(set(result))
|
||||
|
||||
return result
|
||||
@@ -121,25 +132,27 @@ def run_engine(engine, path):
|
||||
|
||||
def _get_model(model, task=None):
|
||||
global _model_cache
|
||||
key = '|'.join([model, str(task)])
|
||||
key = "|".join([model, str(task)])
|
||||
if key not in _model_cache:
|
||||
if task:
|
||||
if task == 'facial-recognition':
|
||||
if task == "facial-recognition":
|
||||
face_model = FaceAnalysis(
|
||||
name=model, root=cache_folder, allowed_modules=["detection", "recognition"])
|
||||
name=model,
|
||||
root=cache_folder,
|
||||
allowed_modules=["detection", "recognition"],
|
||||
)
|
||||
face_model.prepare(ctx_id=0, det_size=(640, 640))
|
||||
_model_cache[key] = face_model
|
||||
else:
|
||||
_model_cache[key] = pipeline(model=model, task=task)
|
||||
else:
|
||||
_model_cache[key] = SentenceTransformer(
|
||||
model, cache_folder=cache_folder)
|
||||
_model_cache[key] = SentenceTransformer(model, cache_folder=cache_folder)
|
||||
return _model_cache[key]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = os.getenv('MACHINE_LEARNING_HOST', '0.0.0.0')
|
||||
port = int(os.getenv('MACHINE_LEARNING_PORT', 3003))
|
||||
is_dev = os.getenv('NODE_ENV') == 'development'
|
||||
host = os.getenv("MACHINE_LEARNING_HOST", "0.0.0.0")
|
||||
port = int(os.getenv("MACHINE_LEARNING_PORT", 3003))
|
||||
is_dev = os.getenv("NODE_ENV") == "development"
|
||||
|
||||
uvicorn.run("main:app", host=host, port=port, reload=is_dev, workers=1)
|
||||
|
||||
@@ -36,7 +36,7 @@ platform :android do
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 79,
|
||||
"android.injected.version.name" => "1.56.1",
|
||||
"android.injected.version.name" => "1.56.2",
|
||||
}
|
||||
)
|
||||
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')
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.56.1"
|
||||
version_number: "1.56.2"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
|
||||
class AssetSelectionState {
|
||||
final Set<String> selectedMonths;
|
||||
final Set<Asset> selectedNewAssetsForAlbum;
|
||||
final Set<Asset> selectedAdditionalAssetsForAlbum;
|
||||
final Set<Asset> selectedAssetsInAlbumViewer;
|
||||
final bool isMultiselectEnable;
|
||||
|
||||
/// Indicate the asset selection page is navigated from existing album
|
||||
final bool isAlbumExist;
|
||||
AssetSelectionState({
|
||||
required this.selectedMonths,
|
||||
required this.selectedNewAssetsForAlbum,
|
||||
required this.selectedAdditionalAssetsForAlbum,
|
||||
required this.selectedAssetsInAlbumViewer,
|
||||
required this.isMultiselectEnable,
|
||||
required this.isAlbumExist,
|
||||
});
|
||||
|
||||
AssetSelectionState copyWith({
|
||||
Set<String>? selectedMonths,
|
||||
Set<Asset>? selectedNewAssetsForAlbum,
|
||||
Set<Asset>? selectedAdditionalAssetsForAlbum,
|
||||
Set<Asset>? selectedAssetsInAlbumViewer,
|
||||
bool? isMultiselectEnable,
|
||||
bool? isAlbumExist,
|
||||
}) {
|
||||
return AssetSelectionState(
|
||||
selectedMonths: selectedMonths ?? this.selectedMonths,
|
||||
selectedNewAssetsForAlbum:
|
||||
selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
|
||||
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ??
|
||||
this.selectedAdditionalAssetsForAlbum,
|
||||
selectedAssetsInAlbumViewer:
|
||||
selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
|
||||
isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
|
||||
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
final setEquals = const DeepCollectionEquality().equals;
|
||||
|
||||
return other is AssetSelectionState &&
|
||||
setEquals(other.selectedMonths, selectedMonths) &&
|
||||
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
|
||||
setEquals(
|
||||
other.selectedAdditionalAssetsForAlbum,
|
||||
selectedAdditionalAssetsForAlbum,
|
||||
) &&
|
||||
setEquals(
|
||||
other.selectedAssetsInAlbumViewer,
|
||||
selectedAssetsInAlbumViewer,
|
||||
) &&
|
||||
other.isMultiselectEnable == isMultiselectEnable &&
|
||||
other.isAlbumExist == isAlbumExist;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return selectedMonths.hashCode ^
|
||||
selectedNewAssetsForAlbum.hashCode ^
|
||||
selectedAdditionalAssetsForAlbum.hashCode ^
|
||||
selectedAssetsInAlbumViewer.hashCode ^
|
||||
isMultiselectEnable.hashCode ^
|
||||
isAlbumExist.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class LogInReponse {
|
||||
final String accessToken;
|
||||
final String userId;
|
||||
final String userEmail;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String profileImagePath;
|
||||
final bool isAdmin;
|
||||
final bool shouldChangePassword;
|
||||
|
||||
LogInReponse({
|
||||
required this.accessToken,
|
||||
required this.userId,
|
||||
required this.userEmail,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.profileImagePath,
|
||||
required this.isAdmin,
|
||||
required this.shouldChangePassword,
|
||||
});
|
||||
|
||||
LogInReponse copyWith({
|
||||
String? accessToken,
|
||||
String? userId,
|
||||
String? userEmail,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? profileImagePath,
|
||||
bool? isAdmin,
|
||||
bool? shouldChangePassword,
|
||||
}) {
|
||||
return LogInReponse(
|
||||
accessToken: accessToken ?? this.accessToken,
|
||||
userId: userId ?? this.userId,
|
||||
userEmail: userEmail ?? this.userEmail,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||
isAdmin: isAdmin ?? this.isAdmin,
|
||||
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
final result = <String, dynamic>{};
|
||||
|
||||
result.addAll({'accessToken': accessToken});
|
||||
result.addAll({'userId': userId});
|
||||
result.addAll({'userEmail': userEmail});
|
||||
result.addAll({'firstName': firstName});
|
||||
result.addAll({'lastName': lastName});
|
||||
result.addAll({'profileImagePath': profileImagePath});
|
||||
result.addAll({'isAdmin': isAdmin});
|
||||
result.addAll({'shouldChangePassword': shouldChangePassword});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
factory LogInReponse.fromMap(Map<String, dynamic> map) {
|
||||
return LogInReponse(
|
||||
accessToken: map['accessToken'] ?? '',
|
||||
userId: map['userId'] ?? '',
|
||||
userEmail: map['userEmail'] ?? '',
|
||||
firstName: map['firstName'] ?? '',
|
||||
lastName: map['lastName'] ?? '',
|
||||
profileImagePath: map['profileImagePath'] ?? '',
|
||||
isAdmin: map['isAdmin'] ?? false,
|
||||
shouldChangePassword: map['shouldChangePassword'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory LogInReponse.fromJson(String source) =>
|
||||
LogInReponse.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is LogInReponse &&
|
||||
other.accessToken == accessToken &&
|
||||
other.userId == userId &&
|
||||
other.userEmail == userEmail &&
|
||||
other.firstName == firstName &&
|
||||
other.lastName == lastName &&
|
||||
other.profileImagePath == profileImagePath &&
|
||||
other.isAdmin == isAdmin &&
|
||||
other.shouldChangePassword == shouldChangePassword;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return accessToken.hashCode ^
|
||||
userId.hashCode ^
|
||||
userEmail.hashCode ^
|
||||
firstName.hashCode ^
|
||||
lastName.hashCode ^
|
||||
profileImagePath.hashCode ^
|
||||
isAdmin.hashCode ^
|
||||
shouldChangePassword.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/cache_settings/cache_settings_slider_pref.dart';
|
||||
import 'package:immich_mobile/shared/services/cache.service.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
|
||||
class CacheSettings extends HookConsumerWidget {
|
||||
const CacheSettings({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final CacheService cacheService = ref.watch(cacheServiceProvider);
|
||||
final clearCacheState = useState(false);
|
||||
|
||||
Future<void> clearCache() async {
|
||||
await cacheService.emptyAllCaches();
|
||||
clearCacheState.value = true;
|
||||
}
|
||||
|
||||
Widget cacheStatisticsRow(String name, CacheType type) {
|
||||
final cacheSize = useState(0);
|
||||
final cacheAssets = useState(0);
|
||||
|
||||
if (!clearCacheState.value) {
|
||||
final repo = cacheService.getCacheRepo(type);
|
||||
|
||||
repo.open().then((_) {
|
||||
cacheSize.value = repo.getCacheSize();
|
||||
cacheAssets.value = repo.getNumberOfCachedObjects();
|
||||
});
|
||||
} else {
|
||||
cacheSize.value = 0;
|
||||
cacheAssets.value = 0;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 20, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
"cache_settings_statistics_assets",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
).tr(
|
||||
args: ["${cacheAssets.value}", formatBytes(cacheSize.value)],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ExpansionTile(
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
textColor: Theme.of(context).primaryColor,
|
||||
title: const Text(
|
||||
'cache_settings_title',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
subtitle: const Text(
|
||||
'cache_settings_subtitle',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
),
|
||||
).tr(),
|
||||
children: [
|
||||
const CacheSettingsSliderPref(
|
||||
setting: AppSettingsEnum.thumbnailCacheSize,
|
||||
translationKey: "cache_settings_thumbnail_size",
|
||||
min: 1000,
|
||||
max: 20000,
|
||||
divisions: 19,
|
||||
),
|
||||
const CacheSettingsSliderPref(
|
||||
setting: AppSettingsEnum.imageCacheSize,
|
||||
translationKey: "cache_settings_image_cache_size",
|
||||
min: 0,
|
||||
max: 1000,
|
||||
divisions: 20,
|
||||
),
|
||||
const CacheSettingsSliderPref(
|
||||
setting: AppSettingsEnum.albumThumbnailCacheSize,
|
||||
translationKey: "cache_settings_album_thumbnails",
|
||||
min: 0,
|
||||
max: 1000,
|
||||
divisions: 20,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
"cache_settings_statistics_title",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
cacheStatisticsRow(
|
||||
"cache_settings_statistics_thumbnail".tr(),
|
||||
CacheType.thumbnail,
|
||||
),
|
||||
cacheStatisticsRow(
|
||||
"cache_settings_statistics_album".tr(),
|
||||
CacheType.albumThumbnail,
|
||||
),
|
||||
cacheStatisticsRow(
|
||||
"cache_settings_statistics_shared".tr(),
|
||||
CacheType.sharedAlbumThumbnail,
|
||||
),
|
||||
cacheStatisticsRow(
|
||||
"cache_settings_statistics_full".tr(),
|
||||
CacheType.imageViewerFull,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(
|
||||
"cache_settings_clear_cache_button_title",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
child: ElevatedButton(
|
||||
onPressed: clearCache,
|
||||
child: const Text(
|
||||
"cache_settings_clear_cache_button",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
).tr(),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
|
||||
class CacheSettingsSliderPref extends HookConsumerWidget {
|
||||
final AppSettingsEnum<int> setting;
|
||||
final String translationKey;
|
||||
final int min;
|
||||
final int max;
|
||||
final int divisions;
|
||||
|
||||
const CacheSettingsSliderPref({
|
||||
Key? key,
|
||||
required this.setting,
|
||||
required this.translationKey,
|
||||
required this.min,
|
||||
required this.max,
|
||||
required this.divisions,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||
|
||||
final itemsValue = useState(appSettingService.getSetting<int>(setting));
|
||||
|
||||
void sliderChanged(double value) {
|
||||
itemsValue.value = value.toInt();
|
||||
}
|
||||
|
||||
void sliderChangedEnd(double value) {
|
||||
appSettingService.setSetting(setting, value.toInt());
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
translationKey,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(args: ["${itemsValue.value.toInt()}"]),
|
||||
),
|
||||
Slider(
|
||||
onChangeEnd: sliderChangedEnd,
|
||||
onChanged: sliderChanged,
|
||||
value: itemsValue.value.toDouble(),
|
||||
min: min.toDouble(),
|
||||
max: max.toDouble(),
|
||||
divisions: divisions,
|
||||
label: "${itemsValue.value.toInt()}",
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class ExperimentalSettings extends HookConsumerWidget {
|
||||
const ExperimentalSettings({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ExpansionTile(
|
||||
textColor: Theme.of(context).primaryColor,
|
||||
title: const Text(
|
||||
'experimental_settings_title',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
).tr(),
|
||||
subtitle: const Text(
|
||||
'experimental_settings_subtitle',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
),
|
||||
).tr(),
|
||||
children: const [
|
||||
// SwitchListTile.adaptive(
|
||||
// activeColor: Theme.of(context).primaryColor,
|
||||
// title: const Text(
|
||||
// "experimental_settings_new_asset_list_title",
|
||||
// style: TextStyle(
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ).tr(),
|
||||
// subtitle: const Text(
|
||||
// "experimental_settings_new_asset_list_subtitle",
|
||||
// style: TextStyle(
|
||||
// fontSize: 12,
|
||||
// ),
|
||||
// ).tr(),
|
||||
// value: useExperimentalAssetGrid.value,
|
||||
// onChanged: changeUseExperimentalAssetGrid,
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
class ImageViewerPageData {
|
||||
final String heroTag;
|
||||
final String imageUrl;
|
||||
final String thumbnailUrl;
|
||||
|
||||
ImageViewerPageData({
|
||||
required this.heroTag,
|
||||
required this.imageUrl,
|
||||
required this.thumbnailUrl,
|
||||
});
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class UploadProfileImageResponse {
|
||||
final String userId;
|
||||
final String profileImagePath;
|
||||
UploadProfileImageResponse({
|
||||
required this.userId,
|
||||
required this.profileImagePath,
|
||||
});
|
||||
|
||||
UploadProfileImageResponse copyWith({
|
||||
String? userId,
|
||||
String? profileImagePath,
|
||||
}) {
|
||||
return UploadProfileImageResponse(
|
||||
userId: userId ?? this.userId,
|
||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
final result = <String, dynamic>{};
|
||||
|
||||
result.addAll({'userId': userId});
|
||||
result.addAll({'profileImagePath': profileImagePath});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
factory UploadProfileImageResponse.fromMap(Map<String, dynamic> map) {
|
||||
return UploadProfileImageResponse(
|
||||
userId: map['userId'] ?? '',
|
||||
profileImagePath: map['profileImagePath'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory UploadProfileImageResponse.fromJson(String source) =>
|
||||
UploadProfileImageResponse.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is UploadProfileImageResponse &&
|
||||
other.userId == userId &&
|
||||
other.profileImagePath == profileImagePath;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => userId.hashCode ^ profileImagePath.hashCode;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/utils/immich_cache_info_repository.dart';
|
||||
|
||||
enum CacheType {
|
||||
// Shared cache for asset thumbnails in various modules
|
||||
thumbnail,
|
||||
imageViewerPreview,
|
||||
imageViewerFull,
|
||||
albumThumbnail,
|
||||
sharedAlbumThumbnail;
|
||||
}
|
||||
|
||||
final cacheServiceProvider = Provider(
|
||||
(ref) => CacheService(ref.watch(appSettingsServiceProvider)),
|
||||
);
|
||||
|
||||
class CacheService {
|
||||
final AppSettingsService _settingsService;
|
||||
final _cacheRepositoryInstances = <CacheType, ImmichCacheRepository>{};
|
||||
|
||||
CacheService(this._settingsService);
|
||||
|
||||
BaseCacheManager getCache(CacheType type) {
|
||||
return _getDefaultCache(
|
||||
type.name,
|
||||
_getCacheSize(type) + 1,
|
||||
getCacheRepo(type),
|
||||
);
|
||||
}
|
||||
|
||||
ImmichCacheRepository getCacheRepo(CacheType type) {
|
||||
if (!_cacheRepositoryInstances.containsKey(type)) {
|
||||
final repo = ImmichCacheInfoRepository(
|
||||
"cache_${type.name}",
|
||||
"cacheKeys_${type.name}",
|
||||
);
|
||||
_cacheRepositoryInstances[type] = repo;
|
||||
}
|
||||
|
||||
return _cacheRepositoryInstances[type]!;
|
||||
}
|
||||
|
||||
Future<void> emptyAllCaches() async {
|
||||
for (var type in CacheType.values) {
|
||||
await getCache(type).emptyCache();
|
||||
}
|
||||
}
|
||||
|
||||
int _getCacheSize(CacheType type) {
|
||||
switch (type) {
|
||||
case CacheType.thumbnail:
|
||||
return _settingsService.getSetting(AppSettingsEnum.thumbnailCacheSize);
|
||||
case CacheType.imageViewerPreview:
|
||||
case CacheType.imageViewerFull:
|
||||
return _settingsService.getSetting(AppSettingsEnum.imageCacheSize);
|
||||
case CacheType.sharedAlbumThumbnail:
|
||||
case CacheType.albumThumbnail:
|
||||
return _settingsService
|
||||
.getSetting(AppSettingsEnum.albumThumbnailCacheSize);
|
||||
default:
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
BaseCacheManager _getDefaultCache(
|
||||
String cacheName,
|
||||
int size,
|
||||
CacheInfoRepository repo,
|
||||
) {
|
||||
return CacheManager(
|
||||
Config(
|
||||
cacheName,
|
||||
maxNrOfCacheObjects: size,
|
||||
repo: repo,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
// ignore_for_file: depend_on_referenced_packages, implementation_imports
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_cache_manager/src/storage/cache_object.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
// Implementation of a CacheInfoRepository based on Hive
|
||||
abstract class ImmichCacheRepository extends CacheInfoRepository {
|
||||
int getNumberOfCachedObjects();
|
||||
int getCacheSize();
|
||||
}
|
||||
|
||||
class ImmichCacheInfoRepository extends ImmichCacheRepository {
|
||||
final String hiveBoxName;
|
||||
final String keyLookupHiveBoxName;
|
||||
|
||||
// To circumvent some of the limitations of a non-relational key-value database,
|
||||
// we use two hive boxes per cache.
|
||||
// [cacheObjectLookupBox] maps ids to cache objects.
|
||||
// [keyLookupHiveBox] maps keys to ids.
|
||||
// The lookup of a cache object by key therefore involves two steps:
|
||||
// id = keyLookupHiveBox[key]
|
||||
// object = cacheObjectLookupBox[id]
|
||||
late Box<Map<dynamic, dynamic>> cacheObjectLookupBox;
|
||||
late Box<int> keyLookupHiveBox;
|
||||
|
||||
ImmichCacheInfoRepository(this.hiveBoxName, this.keyLookupHiveBoxName);
|
||||
|
||||
@override
|
||||
Future<bool> close() async {
|
||||
await cacheObjectLookupBox.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> delete(int id) async {
|
||||
if (cacheObjectLookupBox.containsKey(id)) {
|
||||
await cacheObjectLookupBox.delete(id);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> deleteAll(Iterable<int> ids) async {
|
||||
int deleted = 0;
|
||||
for (var id in ids) {
|
||||
if (cacheObjectLookupBox.containsKey(id)) {
|
||||
deleted++;
|
||||
await cacheObjectLookupBox.delete(id);
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteDataFile() async {
|
||||
await cacheObjectLookupBox.clear();
|
||||
await keyLookupHiveBox.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> exists() async {
|
||||
return cacheObjectLookupBox.isNotEmpty && keyLookupHiveBox.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CacheObject?> get(String key) async {
|
||||
if (!keyLookupHiveBox.containsKey(key)) {
|
||||
return null;
|
||||
}
|
||||
int id = keyLookupHiveBox.get(key)!;
|
||||
if (!cacheObjectLookupBox.containsKey(id)) {
|
||||
keyLookupHiveBox.delete(key);
|
||||
return null;
|
||||
}
|
||||
return _deserialize(cacheObjectLookupBox.get(id)!);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CacheObject>> getAllObjects() async {
|
||||
return cacheObjectLookupBox.values.map(_deserialize).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CacheObject>> getObjectsOverCapacity(int capacity) async {
|
||||
if (cacheObjectLookupBox.length <= capacity) {
|
||||
return List.empty();
|
||||
}
|
||||
var values = cacheObjectLookupBox.values.map(_deserialize).toList();
|
||||
values.sort((CacheObject a, CacheObject b) {
|
||||
final aTouched = a.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
|
||||
final bTouched = b.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
|
||||
|
||||
return aTouched.compareTo(bTouched);
|
||||
});
|
||||
return values.skip(capacity).take(10).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CacheObject>> getOldObjects(Duration maxAge) async {
|
||||
return cacheObjectLookupBox.values
|
||||
.map(_deserialize)
|
||||
.where((CacheObject element) {
|
||||
DateTime touched =
|
||||
element.touched ?? DateTime.fromMicrosecondsSinceEpoch(0);
|
||||
return touched.isBefore(DateTime.now().subtract(maxAge));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CacheObject> insert(
|
||||
CacheObject cacheObject, {
|
||||
bool setTouchedToNow = true,
|
||||
}) async {
|
||||
int newId = keyLookupHiveBox.length == 0
|
||||
? 0
|
||||
: keyLookupHiveBox.values.reduce(max) + 1;
|
||||
cacheObject = cacheObject.copyWith(id: newId);
|
||||
|
||||
keyLookupHiveBox.put(cacheObject.key, newId);
|
||||
cacheObjectLookupBox.put(newId, cacheObject.toMap());
|
||||
|
||||
return cacheObject;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> open() async {
|
||||
cacheObjectLookupBox = await Hive.openBox(hiveBoxName);
|
||||
keyLookupHiveBox = await Hive.openBox(keyLookupHiveBoxName);
|
||||
|
||||
// The cache might have cleared by the operating system.
|
||||
// This could create inconsistencies between the file system cache and database.
|
||||
// To check whether the cache was cleared, a file within the cache directory
|
||||
// is created for each database. If the file is absent, the cache was cleared and therefore
|
||||
// the database has to be cleared as well.
|
||||
if (!await _checkAndCreateAnchorFile()) {
|
||||
await cacheObjectLookupBox.clear();
|
||||
await keyLookupHiveBox.clear();
|
||||
}
|
||||
|
||||
return cacheObjectLookupBox.isOpen;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> update(
|
||||
CacheObject cacheObject, {
|
||||
bool setTouchedToNow = true,
|
||||
}) async {
|
||||
if (cacheObject.id != null) {
|
||||
cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future updateOrInsert(CacheObject cacheObject) {
|
||||
if (cacheObject.id == null) {
|
||||
return insert(cacheObject);
|
||||
} else {
|
||||
return update(cacheObject);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int getNumberOfCachedObjects() {
|
||||
return cacheObjectLookupBox.length;
|
||||
}
|
||||
|
||||
@override
|
||||
int getCacheSize() {
|
||||
final cacheElementsWithSize =
|
||||
cacheObjectLookupBox.values.map(_deserialize).map((e) => e.length ?? 0);
|
||||
|
||||
if (cacheElementsWithSize.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cacheElementsWithSize.reduce((value, element) => value + element);
|
||||
}
|
||||
|
||||
CacheObject _deserialize(Map serData) {
|
||||
Map<String, dynamic> converted = {};
|
||||
|
||||
serData.forEach((key, value) {
|
||||
converted[key.toString()] = value;
|
||||
});
|
||||
|
||||
return CacheObject.fromMap(converted);
|
||||
}
|
||||
|
||||
Future<bool> _checkAndCreateAnchorFile() async {
|
||||
final tmpDir = await getTemporaryDirectory();
|
||||
final cacheFile = File(p.join(tmpDir.path, "$hiveBoxName.tmp"));
|
||||
|
||||
if (await cacheFile.exists()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await cacheFile.create();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.56.1
|
||||
- API version: 1.56.2
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.56.1+79
|
||||
version: 1.56.2+79
|
||||
isar_version: &isar_version 3.0.5
|
||||
|
||||
environment:
|
||||
|
||||
@@ -4095,7 +4095,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.56.1",
|
||||
"version": "1.56.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.56.1",
|
||||
"version": "1.56.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.56.1",
|
||||
"version": "1.56.2",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.56.1",
|
||||
"version": "1.56.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
2
web/src/api/open-api/api.ts
generated
2
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.56.1
|
||||
* The version of the OpenAPI document: 1.56.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.56.1
|
||||
* The version of the OpenAPI document: 1.56.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.56.1
|
||||
* The version of the OpenAPI document: 1.56.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.56.1
|
||||
* The version of the OpenAPI document: 1.56.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.56.1
|
||||
* The version of the OpenAPI document: 1.56.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
Reference in New Issue
Block a user