Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 736b968eab | |||
| d5d8426bda | |||
| be4a783845 | |||
| c30cd3b378 | |||
| 0d3cc28f45 | |||
| f004487be0 | |||
| 21231d53a5 | |||
| a99862120d | |||
| 776023b149 | |||
| 7d4187962a | |||
| a93534fc3c | |||
| cef84f6ced | |||
| a2180a467d | |||
| 1e3dceea4d | |||
| fd4514711f | |||
| 2dd7c13b88 | |||
| 40931b5668 | |||
| 7e92ef9428 | |||
| 372fae20d9 | |||
| 16543a233b | |||
| 73b961f5fa | |||
| e0d15c96f1 |
@@ -37,15 +37,15 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: 'zulu'
|
||||||
java-version: "11.0.21+9"
|
java-version: '17'
|
||||||
cache: "gradle"
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: 'stable'
|
||||||
flutter-version: "3.19.3"
|
flutter-version: '3.19.3'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Create the Keystore
|
- name: Create the Keystore
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ jobs:
|
|||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version: '3.16.9'
|
flutter-version: '3.19.3'
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
|
|||||||
Generated
+1
-1
@@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -572,6 +572,22 @@ describe('/asset', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tests = [
|
const tests = [
|
||||||
|
{
|
||||||
|
input: 'formats/avif/8bit-sRGB.avif',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: '8bit-sRGB.avif',
|
||||||
|
resized: true,
|
||||||
|
exifInfo: {
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: 1080,
|
||||||
|
exifImageWidth: 1617,
|
||||||
|
fileSizeInByte: 862_424,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: 'formats/jpg/el_torcal_rocks.jpg',
|
input: 'formats/jpg/el_torcal_rocks.jpg',
|
||||||
expected: {
|
expected: {
|
||||||
@@ -596,6 +612,22 @@ describe('/asset', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/jxl/8bit-sRGB.jxl',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: '8bit-sRGB.jxl',
|
||||||
|
resized: true,
|
||||||
|
exifInfo: {
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: 1080,
|
||||||
|
exifImageWidth: 1440,
|
||||||
|
fileSizeInByte: 1_780_777,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: 'formats/heic/IMG_2682.heic',
|
input: 'formats/heic/IMG_2682.heic',
|
||||||
expected: {
|
expected: {
|
||||||
@@ -681,6 +713,80 @@ describe('/asset', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: '4_3.rw2',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2018-05-10T08:42:37.842Z',
|
||||||
|
exifInfo: {
|
||||||
|
make: 'Panasonic',
|
||||||
|
model: 'DMC-GH4',
|
||||||
|
exifImageHeight: 3456,
|
||||||
|
exifImageWidth: 4608,
|
||||||
|
exposureTime: '1/100',
|
||||||
|
fNumber: 3.2,
|
||||||
|
focalLength: 35,
|
||||||
|
iso: 400,
|
||||||
|
fileSizeInByte: 19_587_072,
|
||||||
|
dateTimeOriginal: '2018-05-10T08:42:37.842Z',
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
orientation: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: '12bit-compressed-(3_2).arw',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2016-09-27T10:51:44.000Z',
|
||||||
|
exifInfo: {
|
||||||
|
make: 'SONY',
|
||||||
|
model: 'ILCE-6300',
|
||||||
|
exifImageHeight: 4024,
|
||||||
|
exifImageWidth: 6048,
|
||||||
|
exposureTime: '1/320',
|
||||||
|
fNumber: 8,
|
||||||
|
focalLength: 97,
|
||||||
|
iso: 100,
|
||||||
|
lensModel: 'E PZ 18-105mm F4 G OSS',
|
||||||
|
fileSizeInByte: 25_001_984,
|
||||||
|
dateTimeOriginal: '2016-09-27T10:51:44.000Z',
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
orientation: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: '14bit-uncompressed-(3_2).arw',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2016-01-08T15:08:01.000Z',
|
||||||
|
exifInfo: {
|
||||||
|
make: 'SONY',
|
||||||
|
model: 'ILCE-7M2',
|
||||||
|
exifImageHeight: 4024,
|
||||||
|
exifImageWidth: 6048,
|
||||||
|
exposureTime: '1.3',
|
||||||
|
fNumber: 22,
|
||||||
|
focalLength: 25,
|
||||||
|
iso: 100,
|
||||||
|
lensModel: 'E 25mm F2',
|
||||||
|
fileSizeInByte: 49_512_448,
|
||||||
|
dateTimeOriginal: '2016-01-08T15:08:01.000Z',
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
orientation: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { input, expected } of tests) {
|
for (const { input, expected } of tests) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.102.2"
|
version = "1.102.3"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
id "kotlin-kapt"
|
||||||
|
}
|
||||||
|
|
||||||
def localProperties = new Properties()
|
def localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
if (localPropertiesFile.exists()) {
|
||||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
localPropertiesFile.withInputStream { localProperties.load(it) }
|
||||||
localProperties.load(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
|
||||||
if (flutterRoot == null) {
|
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
@@ -21,18 +21,12 @@ if (flutterVersionName == null) {
|
|||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
def keystoreProperties = new Properties()
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 34
|
compileSdkVersion 34
|
||||||
|
|
||||||
@@ -50,7 +44,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId "app.alextran.immich"
|
applicationId "app.alextran.immich"
|
||||||
minSdkVersion 26
|
minSdkVersion 26
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
@@ -88,9 +81,16 @@ flutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
def kotlin_version = '1.9.23'
|
||||||
|
def kotlin_coroutines_version = '1.8.0'
|
||||||
|
def work_version = '2.9.0'
|
||||||
|
def concurrent_version = '1.1.0'
|
||||||
|
def guava_version = '33.1.0-android'
|
||||||
|
def glide_version = '4.16.0'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
implementation "androidx.work:work-runtime:$work_version"
|
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||||
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
|
||||||
implementation "com.google.guava:guava:$guava_version"
|
implementation "com.google.guava:guava:$guava_version"
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
|
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
|
||||||
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
|
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
|
||||||
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
|
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
|
||||||
.putString(BackupWorker.SHARED_PREF_SERVER_URL, args.get(3) as String)
|
|
||||||
.apply()
|
.apply()
|
||||||
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
|
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import android.os.PowerManager
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.concurrent.futures.CallbackToFutureAdapter
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.concurrent.futures.ResolvableFuture
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
@@ -30,16 +30,6 @@ import io.flutter.embedding.engine.loader.FlutterLoader
|
|||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.view.FlutterCallbackInformation
|
import io.flutter.view.FlutterCallbackInformation
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +42,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
*/
|
*/
|
||||||
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
|
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
|
||||||
|
|
||||||
|
private val resolvableFuture = ResolvableFuture.create<Result>()
|
||||||
private var engine: FlutterEngine? = null
|
private var engine: FlutterEngine? = null
|
||||||
private lateinit var backgroundChannel: MethodChannel
|
private lateinit var backgroundChannel: MethodChannel
|
||||||
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
@@ -61,80 +52,35 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
private var notificationDetailBuilder: NotificationCompat.Builder? = null
|
private var notificationDetailBuilder: NotificationCompat.Builder? = null
|
||||||
private var fgFuture: ListenableFuture<Void>? = null
|
private var fgFuture: ListenableFuture<Void>? = null
|
||||||
|
|
||||||
private val job = Job()
|
|
||||||
private lateinit var completer: CallbackToFutureAdapter.Completer<Result>
|
|
||||||
private val resolvableFuture = CallbackToFutureAdapter.getFuture { completer ->
|
|
||||||
this.completer = completer
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
resolvableFuture.addListener(
|
|
||||||
Runnable {
|
|
||||||
if (resolvableFuture.isCancelled) {
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
taskExecutor.serialTaskExecutor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||||
|
|
||||||
Log.d(TAG, "startWork")
|
Log.d(TAG, "startWork")
|
||||||
|
|
||||||
val ctx = applicationContext
|
val ctx = applicationContext
|
||||||
val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
prefs.getString(SHARED_PREF_SERVER_URL, null)
|
if (!flutterLoader.initialized()) {
|
||||||
?.takeIf { it.isNotEmpty() }
|
flutterLoader.startInitialization(ctx)
|
||||||
?.let { serverUrl -> doCoroutineWork(serverUrl) }
|
|
||||||
?: doWork()
|
|
||||||
return resolvableFuture
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is used to check if server URL is reachable before starting the backup work.
|
|
||||||
* Check must be done in a background to avoid blocking the main thread.
|
|
||||||
*/
|
|
||||||
private fun doCoroutineWork(serverUrl : String) {
|
|
||||||
CoroutineScope(Dispatchers.Default + job).launch {
|
|
||||||
val isReachable = isUrlReachableHttp(serverUrl)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (isReachable) {
|
|
||||||
doWork()
|
|
||||||
} else {
|
|
||||||
// Fail when the URL is not reachable
|
|
||||||
completer.set(Result.failure())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Create a Notification channel if necessary
|
||||||
|
createChannel()
|
||||||
|
}
|
||||||
|
if (isIgnoringBatteryOptimizations) {
|
||||||
|
// normal background services can only up to 10 minutes
|
||||||
|
// foreground services are allowed to run indefinitely
|
||||||
|
// requires battery optimizations to be disabled (either manually by the user
|
||||||
|
// or by the system learning that immich is important to the user)
|
||||||
|
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
|
||||||
|
showInfo(getInfoBuilder(title, indeterminate=true).build())
|
||||||
|
}
|
||||||
|
engine = FlutterEngine(ctx)
|
||||||
|
|
||||||
private fun doWork() {
|
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
||||||
Log.d(TAG, "doWork")
|
runDart()
|
||||||
val ctx = applicationContext
|
}
|
||||||
|
|
||||||
if (!flutterLoader.initialized()) {
|
return resolvableFuture
|
||||||
flutterLoader.startInitialization(ctx)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
// Create a Notification channel if necessary
|
|
||||||
createChannel()
|
|
||||||
}
|
|
||||||
if (isIgnoringBatteryOptimizations) {
|
|
||||||
// normal background services can only up to 10 minutes
|
|
||||||
// foreground services are allowed to run indefinitely
|
|
||||||
// requires battery optimizations to be disabled (either manually by the user
|
|
||||||
// or by the system learning that immich is important to the user)
|
|
||||||
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
|
|
||||||
showInfo(getInfoBuilder(title, indeterminate=true).build())
|
|
||||||
}
|
|
||||||
engine = FlutterEngine(ctx)
|
|
||||||
|
|
||||||
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
|
|
||||||
runDart()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -193,7 +139,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
engine = null
|
engine = null
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
Log.d(TAG, "stopEngine result=${result}")
|
Log.d(TAG, "stopEngine result=${result}")
|
||||||
this.completer.set(result)
|
resolvableFuture.set(result)
|
||||||
}
|
}
|
||||||
waitOnSetForegroundAsync()
|
waitOnSetForegroundAsync()
|
||||||
}
|
}
|
||||||
@@ -324,7 +270,6 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
|
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
|
||||||
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
|
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
|
||||||
const val SHARED_PREF_LAST_CHANGE = "lastChange"
|
const val SHARED_PREF_LAST_CHANGE = "lastChange"
|
||||||
const val SHARED_PREF_SERVER_URL = "serverUrl"
|
|
||||||
|
|
||||||
private const val TASK_NAME_BACKUP = "immich/BackupWorker"
|
private const val TASK_NAME_BACKUP = "immich/BackupWorker"
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
|
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
|
||||||
@@ -401,7 +346,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
.setRequiresBatteryNotLow(true)
|
.setRequiresBatteryNotLow(true)
|
||||||
.setRequiresCharging(requireCharging)
|
.setRequiresCharging(requireCharging)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
|
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
|
||||||
@@ -415,26 +360,3 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val TAG = "BackupWorker"
|
private const val TAG = "BackupWorker"
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given URL is reachable via HTTP
|
|
||||||
*/
|
|
||||||
suspend fun isUrlReachableHttp(url: String, timeoutMillis: Long = 5000L): Boolean {
|
|
||||||
return withTimeoutOrNull(timeoutMillis) {
|
|
||||||
var httpURLConnection: HttpURLConnection? = null
|
|
||||||
try {
|
|
||||||
httpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply {
|
|
||||||
requestMethod = "HEAD"
|
|
||||||
connectTimeout = timeoutMillis.toInt()
|
|
||||||
readTimeout = timeoutMillis.toInt()
|
|
||||||
}
|
|
||||||
httpURLConnection.connect()
|
|
||||||
httpURLConnection.responseCode == HttpURLConnection.HTTP_OK
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to reach server URL: $e")
|
|
||||||
false
|
|
||||||
} finally {
|
|
||||||
httpURLConnection?.disconnect()
|
|
||||||
}
|
|
||||||
} == true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +1,3 @@
|
|||||||
buildscript {
|
|
||||||
ext.kotlin_version = '1.8.22'
|
|
||||||
ext.kotlin_coroutines_version = '1.7.1'
|
|
||||||
ext.work_version = '2.9.0'
|
|
||||||
ext.concurrent_version = '1.1.0'
|
|
||||||
ext.guava_version = '33.0.0-android'
|
|
||||||
ext.glide_version = '4.14.2'
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -34,3 +16,7 @@ subprojects {
|
|||||||
tasks.register("clean", Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('wrapper') {
|
||||||
|
distributionType = Wrapper.DistributionType.ALL
|
||||||
|
}
|
||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 134,
|
"android.injected.version.code" => 136,
|
||||||
"android.injected.version.name" => "1.102.2",
|
"android.injected.version.name" => "1.102.3",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000239">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000261">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="71.670134">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="32.48099">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.759668">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.236974">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip
|
||||||
distributionSha256Sum=6001aba9b2204d26fa25a5800bb9382cf3ee01ccb78fe77317b2872336eb2f80
|
distributionSha256Sum=fe696c020f241a5f69c30f763c5a7f38eec54b490db19cd2b0962dda420d7d12
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
include ':app'
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}()
|
||||||
|
|
||||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
def properties = new Properties()
|
|
||||||
|
|
||||||
assert localPropertiesFile.exists()
|
repositories {
|
||||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
plugins {
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
id "com.android.application" version "7.4.2" apply false
|
||||||
|
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
|
||||||
|
id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
||||||
|
|||||||
@@ -296,6 +296,7 @@
|
|||||||
"motion_photos_page_title": "Motion Photos",
|
"motion_photos_page_title": "Motion Photos",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
|
"no_assets_to_show" : "No assets to show",
|
||||||
"notification_permission_dialog_cancel": "Cancel",
|
"notification_permission_dialog_cancel": "Cancel",
|
||||||
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
||||||
"notification_permission_dialog_settings": "Settings",
|
"notification_permission_dialog_settings": "Settings",
|
||||||
|
|||||||
@@ -296,6 +296,7 @@
|
|||||||
"motion_photos_page_title": "Photos avec mouvement",
|
"motion_photos_page_title": "Photos avec mouvement",
|
||||||
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
|
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
|
||||||
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
|
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
|
||||||
|
"no_assets_to_show" : "Aucun élément à afficher",
|
||||||
"notification_permission_dialog_cancel": "Annuler",
|
"notification_permission_dialog_cancel": "Annuler",
|
||||||
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
|
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
|
||||||
"notification_permission_dialog_settings": "Paramètres",
|
"notification_permission_dialog_settings": "Paramètres",
|
||||||
@@ -509,5 +510,7 @@
|
|||||||
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
|
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
|
||||||
"viewer_remove_from_stack": "Retirer de la pile",
|
"viewer_remove_from_stack": "Retirer de la pile",
|
||||||
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
|
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
|
||||||
"viewer_unstack": "Désempiler"
|
"viewer_unstack": "Désempiler",
|
||||||
|
"haptic_feedback_title": "Retour haptique",
|
||||||
|
"haptic_feedback_switch": "Activer le retour haptique"
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.102.1</string>
|
<string>1.102.2</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.102.2"
|
version_number: "1.102.3"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,27 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000226">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000231">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.160617">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.155919">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="2.966255">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.252784">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.184278">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.210502">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="160.892368">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="175.813647">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="73.512517">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import 'package:immich_mobile/shared/models/store.dart';
|
|||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
@@ -69,10 +68,8 @@ class BackgroundService {
|
|||||||
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
|
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
|
||||||
final String title =
|
final String title =
|
||||||
"backup_background_service_default_notification".tr();
|
"backup_background_service_default_notification".tr();
|
||||||
final bool ok = await _foregroundChannel.invokeMethod(
|
final bool ok = await _foregroundChannel
|
||||||
'enable',
|
.invokeMethod('enable', [callback.toRawHandle(), title, immediate]);
|
||||||
[callback.toRawHandle(), title, immediate, getServerUrl()],
|
|
||||||
);
|
|
||||||
return ok;
|
return ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
const Center(child: ImmichLoadingIndicator());
|
const Center(child: ImmichLoadingIndicator());
|
||||||
|
|
||||||
Widget buildEmptyIndicator() =>
|
Widget buildEmptyIndicator() =>
|
||||||
emptyIndicator ?? const Center(child: Text("No assets to show"));
|
emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|||||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.102.2
|
- API version: 1.102.3
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
+1
-1
@@ -1804,5 +1804,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
flutter: ">=3.16.0"
|
flutter: ">=3.16.0"
|
||||||
|
|||||||
+2
-2
@@ -2,10 +2,10 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.102.2+134
|
version: 1.102.3+136
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|||||||
@@ -7078,7 +7078,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.102.2
|
* 1.102.3
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
|
|||||||
+2
-1
@@ -27,7 +27,8 @@
|
|||||||
"matchFileNames": ["mobile/**"],
|
"matchFileNames": ["mobile/**"],
|
||||||
"groupName": "mobile",
|
"groupName": "mobile",
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
"schedule": "on tuesday"
|
"schedule": "on tuesday",
|
||||||
|
"addLabels": ["📱mobile"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"groupName": "exiftool",
|
"groupName": "exiftool",
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { SessionEntity } from 'src/entities/session.entity';
|
|||||||
|
|
||||||
export const ISessionRepository = 'ISessionRepository';
|
export const ISessionRepository = 'ISessionRepository';
|
||||||
|
|
||||||
|
type E = SessionEntity;
|
||||||
|
|
||||||
export interface ISessionRepository {
|
export interface ISessionRepository {
|
||||||
create(dto: Partial<SessionEntity>): Promise<SessionEntity>;
|
create<T extends Partial<E>>(dto: T): Promise<T>;
|
||||||
update(dto: Partial<SessionEntity>): Promise<SessionEntity>;
|
update<T extends Partial<E>>(dto: T): Promise<T>;
|
||||||
delete(id: string): Promise<void>;
|
delete(id: string): Promise<void>;
|
||||||
getByToken(token: string): Promise<SessionEntity | null>;
|
getByToken(token: string): Promise<E | null>;
|
||||||
getByUserId(userId: string): Promise<SessionEntity[]>;
|
getByUserId(userId: string): Promise<E[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ export class SessionRepository implements ISessionRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
create(session: Partial<SessionEntity>): Promise<SessionEntity> {
|
create<T extends Partial<SessionEntity>>(dto: T): Promise<T & { id: string }> {
|
||||||
return this.repository.save(session);
|
return this.repository.save(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(session: Partial<SessionEntity>): Promise<SessionEntity> {
|
update<T extends Partial<SessionEntity>>(dto: T): Promise<T> {
|
||||||
return this.repository.save(session);
|
return this.repository.save(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
|||||||
@@ -340,10 +340,7 @@ describe('AuthService', () => {
|
|||||||
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive);
|
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive);
|
||||||
sessionMock.update.mockResolvedValue(sessionStub.valid);
|
sessionMock.update.mockResolvedValue(sessionStub.valid);
|
||||||
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
||||||
await expect(sut.validate(headers, {})).resolves.toEqual({
|
await expect(sut.validate(headers, {})).resolves.toBeDefined();
|
||||||
user: userStub.user1,
|
|
||||||
session: sessionStub.valid,
|
|
||||||
});
|
|
||||||
expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
|
expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -374,14 +374,14 @@ export class AuthService {
|
|||||||
|
|
||||||
private async validateSession(tokenValue: string): Promise<AuthDto> {
|
private async validateSession(tokenValue: string): Promise<AuthDto> {
|
||||||
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
|
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
|
||||||
let session = await this.sessionRepository.getByToken(hashedToken);
|
const session = await this.sessionRepository.getByToken(hashedToken);
|
||||||
|
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
const updatedAt = DateTime.fromJSDate(session.updatedAt);
|
const updatedAt = DateTime.fromJSDate(session.updatedAt);
|
||||||
const diff = now.diff(updatedAt, ['hours']);
|
const diff = now.diff(updatedAt, ['hours']);
|
||||||
if (diff.hours > 1) {
|
if (diff.hours > 1) {
|
||||||
session = await this.sessionRepository.update({ id: session.id, updatedAt: new Date() });
|
await this.sessionRepository.update({ id: session.id, updatedAt: new Date() });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user: session.user, session: session };
|
return { user: session.user, session: session };
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ export class AV1Config extends BaseConfig {
|
|||||||
|
|
||||||
export class NVENCConfig extends BaseHWConfig {
|
export class NVENCConfig extends BaseHWConfig {
|
||||||
getSupportedCodecs() {
|
getSupportedCodecs() {
|
||||||
return [VideoCodec.H264, VideoCodec.HEVC];
|
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseInputOptions() {
|
getBaseInputOptions() {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Mocked, vitest } from 'vitest';
|
|||||||
|
|
||||||
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {
|
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {
|
||||||
return {
|
return {
|
||||||
create: vitest.fn(),
|
create: vitest.fn() as any,
|
||||||
update: vitest.fn(),
|
update: vitest.fn() as any,
|
||||||
delete: vitest.fn(),
|
delete: vitest.fn(),
|
||||||
getByToken: vitest.fn(),
|
getByToken: vitest.fn(),
|
||||||
getByUserId: vitest.fn(),
|
getByUserId: vitest.fn(),
|
||||||
|
|||||||
Generated
+21
-3
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.7.1",
|
"@photo-sphere-viewer/core": "^5.7.1",
|
||||||
|
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||||
|
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||||
"@zoom-image/svelte": "^0.2.6",
|
"@zoom-image/svelte": "^0.2.6",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
@@ -1590,6 +1592,22 @@
|
|||||||
"three": "^0.161.0"
|
"three": "^0.161.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
|
||||||
|
"version": "5.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz",
|
||||||
|
"integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@photo-sphere-viewer/core": "5.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@photo-sphere-viewer/video-plugin": {
|
||||||
|
"version": "5.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz",
|
||||||
|
"integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@photo-sphere-viewer/core": "5.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.24",
|
"version": "1.0.0-next.24",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz",
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.102.2",
|
"version": "1.102.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
@@ -61,6 +61,8 @@
|
|||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.7.1",
|
"@photo-sphere-viewer/core": "^5.7.1",
|
||||||
|
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||||
|
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||||
"@zoom-image/svelte": "^0.2.6",
|
"@zoom-image/svelte": "^0.2.6",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
import PanoramaViewer from './panorama-viewer.svelte';
|
import PanoramaViewer from './panorama-viewer.svelte';
|
||||||
import PhotoViewer from './photo-viewer.svelte';
|
import PhotoViewer from './photo-viewer.svelte';
|
||||||
import SlideshowBar from './slideshow-bar.svelte';
|
import SlideshowBar from './slideshow-bar.svelte';
|
||||||
import VideoViewer from './video-viewer.svelte';
|
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||||
|
|
||||||
export let assetStore: AssetStore | null = null;
|
export let assetStore: AssetStore | null = null;
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
@@ -622,6 +622,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={previewStackedAsset.id}
|
assetId={previewStackedAsset.id}
|
||||||
|
projectionType={previewStackedAsset.exifInfo?.projectionType}
|
||||||
on:close={closeViewer}
|
on:close={closeViewer}
|
||||||
on:onVideoEnded={() => navigateAsset()}
|
on:onVideoEnded={() => navigateAsset()}
|
||||||
on:onVideoStarted={handleVideoStarted}
|
on:onVideoStarted={handleVideoStarted}
|
||||||
@@ -642,6 +643,7 @@
|
|||||||
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={asset.livePhotoVideoId}
|
assetId={asset.livePhotoVideoId}
|
||||||
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
on:close={closeViewer}
|
on:close={closeViewer}
|
||||||
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
||||||
/>
|
/>
|
||||||
@@ -655,6 +657,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={asset.id}
|
assetId={asset.id}
|
||||||
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
on:close={closeViewer}
|
on:close={closeViewer}
|
||||||
on:onVideoEnded={() => navigateAsset()}
|
on:onVideoEnded={() => navigateAsset()}
|
||||||
on:onVideoStarted={handleVideoStarted}
|
on:onVideoStarted={handleVideoStarted}
|
||||||
|
|||||||
@@ -1,22 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { serveFile, type AssetResponseDto } from '@immich/sdk';
|
import { serveFile, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import { getKey } from '$lib/utils';
|
import { getKey } from '$lib/utils';
|
||||||
export let asset: AssetResponseDto;
|
import type { AdapterConstructor, PluginConstructor } from '@photo-sphere-viewer/core';
|
||||||
|
export let asset: Pick<AssetResponseDto, 'id' | 'type'>;
|
||||||
|
|
||||||
|
const photoSphereConfigs =
|
||||||
|
asset.type === AssetTypeEnum.Video
|
||||||
|
? ([
|
||||||
|
import('@photo-sphere-viewer/equirectangular-video-adapter').then(
|
||||||
|
({ EquirectangularVideoAdapter }) => EquirectangularVideoAdapter,
|
||||||
|
),
|
||||||
|
import('@photo-sphere-viewer/video-plugin').then(({ VideoPlugin }) => [VideoPlugin]),
|
||||||
|
true,
|
||||||
|
import('@photo-sphere-viewer/video-plugin/index.css'),
|
||||||
|
] as [PromiseLike<AdapterConstructor>, Promise<PluginConstructor[]>, true, unknown])
|
||||||
|
: ([undefined, [], false] as [undefined, [], false]);
|
||||||
|
|
||||||
const loadAssetData = async () => {
|
const loadAssetData = async () => {
|
||||||
const data = await serveFile({ id: asset.id, isWeb: false, isThumb: false, key: getKey() });
|
const data = await serveFile({ id: asset.id, isWeb: false, isThumb: false, key: getKey() });
|
||||||
return URL.createObjectURL(data);
|
const url = URL.createObjectURL(data);
|
||||||
|
if (asset.type === AssetTypeEnum.Video) {
|
||||||
|
return { source: url };
|
||||||
|
}
|
||||||
|
return url;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||||
<!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data -->
|
<!-- the photo sphere viewer is quite large, so lazy load it in parallel with loading the data -->
|
||||||
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte')])}
|
{#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then [data, module]}
|
{:then [data, module, adapter, plugins, navbar]}
|
||||||
<svelte:component this={module.default} panorama={data} />
|
<svelte:component this={module.default} panorama={data} plugins={plugins ?? undefined} {navbar} {adapter} />
|
||||||
{:catch}
|
{:catch}
|
||||||
Failed to load asset
|
Failed to load asset
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Viewer } from '@photo-sphere-viewer/core';
|
import {
|
||||||
|
Viewer,
|
||||||
|
EquirectangularAdapter,
|
||||||
|
type PluginConstructor,
|
||||||
|
type AdapterConstructor,
|
||||||
|
} from '@photo-sphere-viewer/core';
|
||||||
import '@photo-sphere-viewer/core/index.css';
|
import '@photo-sphere-viewer/core/index.css';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
export let panorama: string;
|
export let panorama: string | { source: string };
|
||||||
|
export let adapter: AdapterConstructor | [AdapterConstructor, unknown] = EquirectangularAdapter;
|
||||||
|
export let plugins: (PluginConstructor | [PluginConstructor, unknown])[] = [];
|
||||||
|
export let navbar = false;
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let viewer: Viewer;
|
let viewer: Viewer;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
viewer = new Viewer({
|
viewer = new Viewer({
|
||||||
|
adapter,
|
||||||
|
plugins,
|
||||||
container,
|
container,
|
||||||
panorama,
|
panorama,
|
||||||
navbar: false,
|
touchmoveTwoFingers: true,
|
||||||
|
mousewheelCtrlKey: false,
|
||||||
|
navbar,
|
||||||
|
maxFov: 180,
|
||||||
|
fisheye: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
|
|
||||||
const { slideshowState, slideshowLook } = slideshowStore;
|
const { slideshowState, slideshowLook } = slideshowStore;
|
||||||
|
|
||||||
@@ -150,15 +150,24 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||||
class="flex h-full select-none place-content-center place-items-center"
|
class="relative h-full select-none"
|
||||||
>
|
>
|
||||||
{#if !imageLoaded}
|
{#if !imageLoaded}
|
||||||
<LoadingSpinner />
|
<div class="flex h-full items-center justify-center">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div bind:this={imgElement} class="h-full w-full">
|
<div bind:this={imgElement} class="h-full w-full" transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}>
|
||||||
|
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
||||||
|
<img
|
||||||
|
src={assetData}
|
||||||
|
alt={getAltText(asset)}
|
||||||
|
class="absolute top-0 left-0 -z-10 object-cover h-full w-full blur-lg"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<img
|
<img
|
||||||
bind:this={$photoViewer}
|
bind:this={$photoViewer}
|
||||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
|
||||||
src={assetData}
|
src={assetData}
|
||||||
alt={getAltText(asset)}
|
alt={getAltText(asset)}
|
||||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AssetTypeEnum } from '@immich/sdk';
|
||||||
|
import { ProjectionType } from '$lib/constants';
|
||||||
|
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
|
||||||
|
import PanoramaViewer from '$lib/components/asset-viewer/panorama-viewer.svelte';
|
||||||
|
|
||||||
|
export let assetId: string;
|
||||||
|
export let projectionType: string | null | undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||||
|
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
|
||||||
|
{:else}
|
||||||
|
<VideoNativeViewer {assetId} on:onVideoEnded on:onVideoStarted />
|
||||||
|
{/if}
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
'text-immich-primary dark:text-immich-dark-primary enabled:dark:hover:bg-immich-dark-primary/10 enabled:hover:bg-immich-primary/10',
|
'text-immich-primary dark:text-immich-dark-primary enabled:dark:hover:bg-immich-dark-primary/10 enabled:hover:bg-immich-primary/10',
|
||||||
'light-red': 'bg-[#F9DEDC] text-[#410E0B] enabled:hover:bg-red-50',
|
'light-red': 'bg-[#F9DEDC] text-[#410E0B] enabled:hover:bg-red-50',
|
||||||
red: 'bg-red-500 text-white enabled:hover:bg-red-400',
|
red: 'bg-red-500 text-white enabled:hover:bg-red-400',
|
||||||
green: 'bg-green-500 text-gray-800 enabled:hover:bg-green-400/90',
|
green: 'bg-green-600 text-white enabled:hover:bg-green-400/90',
|
||||||
gray: 'bg-gray-500 dark:bg-gray-200 enabled:hover:bg-gray-500/75 enabled:dark:hover:bg-gray-200/80 text-white dark:text-immich-dark-gray',
|
gray: 'bg-gray-500 dark:bg-gray-200 enabled:hover:bg-gray-500/75 enabled:dark:hover:bg-gray-200/80 text-white dark:text-immich-dark-gray',
|
||||||
'transparent-gray':
|
'transparent-gray':
|
||||||
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',
|
'dark:text-immich-dark-fg enabled:hover:bg-immich-primary/5 enabled:hover:text-gray-700 enabled:hover:dark:text-immich-dark-fg enabled:dark:hover:bg-immich-dark-primary/25',
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
hoverLabel = new Date(attr).toLocaleString($locale, {
|
hoverLabel = new Date(attr).toLocaleString($locale, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
mdiMagnify,
|
mdiMagnify,
|
||||||
mdiMap,
|
mdiMap,
|
||||||
mdiTrashCanOutline,
|
mdiTrashCanOutline,
|
||||||
|
mdiTuneVariant,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import LoadingSpinner from '../loading-spinner.svelte';
|
import LoadingSpinner from '../loading-spinner.svelte';
|
||||||
import StatusBox from '../status-box.svelte';
|
import StatusBox from '../status-box.svelte';
|
||||||
@@ -145,6 +146,12 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</SideBarLink>
|
</SideBarLink>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
|
||||||
|
<p class="hidden p-6 group-hover:sm:block md:block">AUTOMATION</p>
|
||||||
|
<hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
|
||||||
|
</div>
|
||||||
|
<SideBarLink title="Workflows" routeId="/(user)/workflows" icon={mdiTuneVariant} />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Status Box -->
|
<!-- Status Box -->
|
||||||
|
|||||||
@@ -4,7 +4,14 @@
|
|||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiFitToPageOutline, mdiFitToScreenOutline, mdiShuffle } from '@mdi/js';
|
import {
|
||||||
|
mdiArrowDownThin,
|
||||||
|
mdiArrowUpThin,
|
||||||
|
mdiFitToPageOutline,
|
||||||
|
mdiFitToScreenOutline,
|
||||||
|
mdiPanorama,
|
||||||
|
mdiShuffle,
|
||||||
|
} from '@mdi/js';
|
||||||
import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
|
import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
|
||||||
import Button from './elements/buttons/button.svelte';
|
import Button from './elements/buttons/button.svelte';
|
||||||
import type { RenderedOption } from './elements/dropdown.svelte';
|
import type { RenderedOption } from './elements/dropdown.svelte';
|
||||||
@@ -23,6 +30,7 @@
|
|||||||
const lookOptions: Record<SlideshowLook, RenderedOption> = {
|
const lookOptions: Record<SlideshowLook, RenderedOption> = {
|
||||||
[SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' },
|
[SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' },
|
||||||
[SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' },
|
[SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' },
|
||||||
|
[SlideshowLook.BlurredBackground]: { icon: mdiPanorama, title: 'Blurred background' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = <Type extends SlideshowNavigation | SlideshowLook>(
|
const handleToggle = <Type extends SlideshowNavigation | SlideshowLook>(
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { mdiPencil, mdiTrashCan } from '@mdi/js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-zinc-300 dark:bg-zinc-800 text-black dark:text-white min-h-[60px] grid grid-cols-[15%_70%_15%] place-items-center place-content-center rounded-2xl mx-4 mt-2 p-2"
|
||||||
|
>
|
||||||
|
<p class="col-start-2 col-span-1">Add to album "RANDOM"</p>
|
||||||
|
|
||||||
|
<div class="col-start-3 col-span-1 flex gap-2 justify-self-end">
|
||||||
|
<CircleIconButton size="20" padding="2" title="Remove rule" icon={mdiTrashCan} />
|
||||||
|
<CircleIconButton size="20" padding="2" title="Edit rule" icon={mdiPencil} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import { mdiPencil, mdiTrashCan } from '@mdi/js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-zinc-200 text-black dark:bg-zinc-700 dark:text-white min-h-[60px] grid grid-cols-[15%_70%_15%] place-items-center place-content-center rounded-2xl mx-4 mt-2 p-2"
|
||||||
|
>
|
||||||
|
<p class="col-start-2 col-span-1">And has Alex and Henry and Nate</p>
|
||||||
|
|
||||||
|
<div class="col-start-3 col-span-1 flex gap-2 justify-self-end">
|
||||||
|
<CircleIconButton size="20" padding="2" title="Remove rule" icon={mdiTrashCan} />
|
||||||
|
<CircleIconButton size="20" padding="2" title="Edit rule" icon={mdiPencil} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div
|
||||||
|
class="bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-black min-h-[60px] flex place-items-center place-content-center mx-4 mt-2 rounded-2xl"
|
||||||
|
>
|
||||||
|
When an asset is uploaded
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// export let onSelect: () => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-3xl mr-2 my-4 min-h-[60px] grid grid-cols-[32px_1fr] gap-2 place-items-center p-4 place-content-center bg-gray-100 hover:bg-gray-200 dark:bg-gray-900 hover:dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<div class="w-4 h-4 rounded-full bg-green-400"></div>
|
||||||
|
<p class="dark:text-gray-300">Add this photo to every albums and notify everybody about this glorious asset</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import WorkflowActionCard from '$lib/components/workflow-page/editor/workflow-action-card.svelte';
|
||||||
|
import WorkflowRuleCard from '$lib/components/workflow-page/editor/workflow-rule-card.svelte';
|
||||||
|
import WorkflowTriggerCard from '$lib/components/workflow-page/editor/workflow-trigger-card.svelte';
|
||||||
|
import { mdiPlus } from '@mdi/js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="h-full overflow-scroll">
|
||||||
|
<div
|
||||||
|
id="workflow-control-bar"
|
||||||
|
class="sticky top-0 flex justify-between place-items-center border-b border-gray-200 dark:border-gray-800 p-4 bg-zinc-50 dark:bg-zinc-900 z-20"
|
||||||
|
>
|
||||||
|
<p class="uppercase text-lg dark:text-white font-medium">
|
||||||
|
Add this photo to every albums and notify everybody about this glorious asset
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button size="sm" color="red">Discard</Button>
|
||||||
|
<Button size="sm">Disable</Button>
|
||||||
|
<Button size="sm" color="green">Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="workflows-selection">
|
||||||
|
<!-- TRIGGER BLOCK -->
|
||||||
|
<div class="translate-y-3">
|
||||||
|
<p class="pl-4 text-xs dark:text-gray-300">TRIGGER</p>
|
||||||
|
<WorkflowTriggerCard />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VISUAL CONNECTOR -->
|
||||||
|
<div class="relative w-full grid grid-cols-3 place-items-center place-content-center z-10">
|
||||||
|
<p class="col-start-1 col-span-1 justify-self-start self-end pl-4 pb-6 text-xs dark:text-gray-300">RULES</p>
|
||||||
|
<div class="col-start-2 col-span-1 flex flex-col place-items-center">
|
||||||
|
<div
|
||||||
|
class="rounded-full border-[6px] border-immich-primary dark:border-immich-dark-primary h-[20px] w-[20px] bg-white translate-y-1"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="h-[60px] w-[5px] bg-white bg-gradient-to-b from-immich-primary dark:from-immich-dark-primary via-zinc-300 dark:via-gray-600 dark:to-zinc-700"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="rounded-full border-[6px] border-zinc-200 dark:border-gray-700 h-[20px] w-[20px] bg-white -translate-y-1"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RULES BLOCK -->
|
||||||
|
<div id="rule-block" class="-translate-y-6">
|
||||||
|
<WorkflowRuleCard />
|
||||||
|
<WorkflowRuleCard />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border-2 dark:border-gray-700 dark:text-white min-h-[60px] flex place-items-center place-content-center rounded-2xl mx-4 mt-2"
|
||||||
|
>
|
||||||
|
<span><Icon path={mdiPlus} /></span> ADD RULE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VISUAL CONNECTOR -->
|
||||||
|
<div class="relative w-full grid grid-cols-3 place-items-center place-content-center -translate-y-9 z-10">
|
||||||
|
<p class="col-start-1 col-span-1 justify-self-start self-end pl-4 pb-6 text-xs dark:text-gray-300">ACTIONS</p>
|
||||||
|
<div class="col-start-2 col-span-1 flex flex-col place-items-center">
|
||||||
|
<div class="rounded-full border-[6px] border-gray-900 h-[20px] w-[20px] bg-white translate-y-1"></div>
|
||||||
|
<div class="h-[60px] w-[5px] bg-white bg-gradient-to-b from-gray-900 via-indigo-800 to-gray-700"></div>
|
||||||
|
<div class="rounded-full border-[6px] border-gray-700 h-[20px] w-[20px] bg-white -translate-y-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ACTION BLOCK -->
|
||||||
|
<div id="action-block" class="-translate-y-14">
|
||||||
|
<WorkflowActionCard />
|
||||||
|
<WorkflowActionCard />
|
||||||
|
<div
|
||||||
|
class="border-2 dark:border-gray-500 dark:text-white min-h-[60px] flex place-items-center place-content-center rounded-2xl mx-4 mt-2"
|
||||||
|
>
|
||||||
|
<span><Icon path={mdiPlus} /></span> ADD ACTION
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import WorkflowCard from '$lib/components/workflow-page/workflow-card.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section id="workflow-list" class="border-r border-gray-200 dark:border-gray-800 h-full relative overflow-scroll pr-2">
|
||||||
|
<div class="sticky top-0 dark:bg-immich-dark-bg flex justify-between place-items-center pr-2 py-4 bg-immich-bg">
|
||||||
|
<p class="text-xs dark:text-white">CURRENT WORKFLOWS</p>
|
||||||
|
<Button size="sm">New Workflow</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each Array.from({ length: 50 }) as _}
|
||||||
|
<WorkflowCard />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -16,11 +16,13 @@ export enum SlideshowNavigation {
|
|||||||
export enum SlideshowLook {
|
export enum SlideshowLook {
|
||||||
Contain = 'contain',
|
Contain = 'contain',
|
||||||
Cover = 'cover',
|
Cover = 'cover',
|
||||||
|
BlurredBackground = 'blurred-background',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const slideshowLookCssMapping: Record<SlideshowLook, string> = {
|
export const slideshowLookCssMapping: Record<SlideshowLook, string> = {
|
||||||
[SlideshowLook.Contain]: 'object-contain',
|
[SlideshowLook.Contain]: 'object-contain',
|
||||||
[SlideshowLook.Cover]: 'object-cover',
|
[SlideshowLook.Cover]: 'object-cover',
|
||||||
|
[SlideshowLook.BlurredBackground]: 'object-contain',
|
||||||
};
|
};
|
||||||
|
|
||||||
function createSlideshowStore() {
|
function createSlideshowStore() {
|
||||||
|
|||||||
@@ -39,8 +39,12 @@
|
|||||||
try {
|
try {
|
||||||
await emptyTrash();
|
await emptyTrash();
|
||||||
|
|
||||||
|
const deletedAssetIds = assetStore.assets.map((a) => a.id);
|
||||||
|
const numberOfAssets = deletedAssetIds.length;
|
||||||
|
assetStore.removeAssets(deletedAssetIds);
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Empty trash initiated. Refresh the page to see the changes`,
|
message: `Permanently deleted ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -52,8 +56,12 @@
|
|||||||
try {
|
try {
|
||||||
await restoreTrash();
|
await restoreTrash();
|
||||||
|
|
||||||
|
const restoredAssetIds = assetStore.assets.map((a) => a.id);
|
||||||
|
const numberOfAssets = restoredAssetIds.length;
|
||||||
|
assetStore.removeAssets(restoredAssetIds);
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Restore trash initiated. Refresh the page to see the changes`,
|
message: `Restored ${numberOfAssets} ${numberOfAssets == 1 ? 'asset' : 'assets'}`,
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
|
import WorkflowEditor from '$lib/components/workflow-page/workflow-editor.svelte';
|
||||||
|
import WorkflowList from '$lib/components/workflow-page/workflow-list.svelte';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<UserPageLayout title={data.meta.title} scrollbar={false}>
|
||||||
|
<section class="grid grid-cols-[25%_1fr] h-full">
|
||||||
|
<WorkflowList />
|
||||||
|
<WorkflowEditor />
|
||||||
|
</section>
|
||||||
|
</UserPageLayout>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { authenticate } from '$lib/utils/auth';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async () => {
|
||||||
|
await authenticate();
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta: {
|
||||||
|
title: 'Workflows',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
||||||
Reference in New Issue
Block a user