mirror of
https://github.com/immich-app/immich.git
synced 2026-07-09 13:49:37 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b0dda65cc | |||
| 443b856ac3 |
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v3.0.1",
|
||||
"url": "https://docs.v3.0.1.archive.immich.app"
|
||||
"label": "v3.0.2",
|
||||
"url": "https://docs.v3.0.2.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.7.5",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
Generated
+1
-1
@@ -974,7 +974,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "3.0.1"
|
||||
version = "3.0.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -18,6 +18,7 @@ import app.alextran.immich.images.LocalImageApi
|
||||
import app.alextran.immich.images.LocalImagesImpl
|
||||
import app.alextran.immich.images.RemoteImageApi
|
||||
import app.alextran.immich.images.RemoteImagesImpl
|
||||
import app.alextran.immich.mediasave.MediaSavePlugin
|
||||
import app.alextran.immich.permission.PermissionApi
|
||||
import app.alextran.immich.permission.PermissionApiImpl
|
||||
import app.alextran.immich.sync.NativeSyncApi
|
||||
@@ -63,6 +64,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
|
||||
|
||||
flutterEngine.plugins.add(ViewIntentPlugin())
|
||||
flutterEngine.plugins.add(MediaSavePlugin())
|
||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||
flutterEngine.plugins.add(permissionApiImpl)
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
package app.alextran.immich.mediasave
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.BasicMessageChannel
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MessageCodec
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
private object MediaSavePigeonUtils {
|
||||
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||
* @property code The error code.
|
||||
* @property message The error message.
|
||||
* @property details The error details. Must be a datatype supported by the api codec.
|
||||
*/
|
||||
class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : RuntimeException()
|
||||
private open class MediaSavePigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return super.readValueOfType(type, buffer)
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface MediaSaveApi {
|
||||
fun saveToDownloads(filePath: String, title: String, relativePath: String?, callback: (Result<String?>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by MediaSaveApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
MediaSavePigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `MediaSaveApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: MediaSaveApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.MediaSaveApi.saveToDownloads$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val filePathArg = args[0] as String
|
||||
val titleArg = args[1] as String
|
||||
val relativePathArg = args[2] as String?
|
||||
api.saveToDownloads(filePathArg, titleArg, relativePathArg) { result: Result<String?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MediaSavePigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MediaSavePigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package app.alextran.immich.mediasave
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MediaSavePlugin : FlutterPlugin, MediaSaveApi {
|
||||
private var context: Context? = null
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = binding.applicationContext
|
||||
MediaSaveApi.setUp(binding.binaryMessenger, this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
MediaSaveApi.setUp(binding.binaryMessenger, null)
|
||||
ioScope.cancel()
|
||||
context = null
|
||||
}
|
||||
|
||||
override fun saveToDownloads(
|
||||
filePath: String,
|
||||
title: String,
|
||||
relativePath: String?,
|
||||
callback: (Result<String?>) -> Unit,
|
||||
) {
|
||||
val context = context ?: run {
|
||||
callback(Result.success(null))
|
||||
return
|
||||
}
|
||||
|
||||
ioScope.launch {
|
||||
try {
|
||||
callback(Result.success(insertIntoFiles(context, filePath, title, relativePath)))
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uses the Files collection, not Images: Images only accepts MIME types the
|
||||
// platform knows and rejects raw formats like CR3, while Files accepts any
|
||||
// type. The file lands under [relativePath] (Download/Immich), not the gallery.
|
||||
private fun insertIntoFiles(
|
||||
context: Context,
|
||||
filePath: String,
|
||||
title: String,
|
||||
relativePath: String?,
|
||||
): String? {
|
||||
val resolver = context.contentResolver
|
||||
val collection = MediaStore.Files.getContentUri("external")
|
||||
val source = File(filePath)
|
||||
// Anything reaching this fallback is a format the platform can't type, so
|
||||
// store it as a generic binary. The file saves and stays openable.
|
||||
val mimeType = "application/octet-stream"
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, title)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||
relativePath?.let { put(MediaStore.MediaColumns.RELATIVE_PATH, it) }
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
val uri = resolver.insert(collection, values) ?: return null
|
||||
|
||||
try {
|
||||
val out = resolver.openOutputStream(uri)
|
||||
if (out == null) {
|
||||
resolver.delete(uri, null, null)
|
||||
return null
|
||||
}
|
||||
out.use { source.inputStream().use { input -> input.copyTo(it) } }
|
||||
resolver.update(uri, ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }, null, null)
|
||||
return uri.lastPathSegment
|
||||
} catch (e: Exception) {
|
||||
resolver.delete(uri, null, null)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
val dir = File(Environment.getExternalStorageDirectory(), relativePath ?: Environment.DIRECTORY_DCIM).apply { mkdirs() }
|
||||
val target = File(dir, title)
|
||||
source.inputStream().use { input -> target.outputStream().use { input.copyTo(it) } }
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, title)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
|
||||
@Suppress("DEPRECATION")
|
||||
put(MediaStore.MediaColumns.DATA, target.absolutePath)
|
||||
}
|
||||
return resolver.insert(collection, values)?.lastPathSegment
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3054,
|
||||
"android.injected.version.name" => "3.0.1",
|
||||
"android.injected.version.code" => 3055,
|
||||
"android.injected.version.name" => "3.0.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', track: 'beta')
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3054,
|
||||
"android.injected.version.name" => "3.0.1",
|
||||
"android.injected.version.code" => 3055,
|
||||
"android.injected.version.name" => "3.0.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')
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.1</string>
|
||||
<string>3.0.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
void writeValue(WriteBuffer buffer, Object? value) {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MediaSaveApi {
|
||||
/// Constructor for [MediaSaveApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
MediaSaveApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<String?> saveToDownloads(String filePath, String title, String? relativePath) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.MediaSaveApi.saveToDownloads$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[filePath, title, relativePath]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
return pigeonVar_replyValue as String?;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/platform/media_save_api.g.dart';
|
||||
import 'package:immich_mobile/utils/mime.utils.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:photo_manager/photo_manager.dart' hide AssetType;
|
||||
|
||||
final _mediaSaveApi = MediaSaveApi();
|
||||
|
||||
final fileMediaRepositoryProvider = Provider((ref) => const FileMediaRepository());
|
||||
|
||||
class FileMediaRepository {
|
||||
@@ -25,8 +31,19 @@ class FileMediaRepository {
|
||||
}
|
||||
|
||||
Future<AssetEntity?> saveImageWithFile(String filePath, {String? title, String? relativePath}) async {
|
||||
final entity = await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath);
|
||||
return entity;
|
||||
try {
|
||||
return await PhotoManager.editor.saveImageWithPath(filePath, title: title, relativePath: relativePath);
|
||||
} on PlatformException catch (e) {
|
||||
// Some formats (e.g. raw like CR3) have no MIME the platform recognises, so
|
||||
// photo_manager falls back to `image/*` and MediaStore rejects the save.
|
||||
// Save those to Downloads ourselves, where the Files collection takes any
|
||||
// type. Anything else isn't ours to handle.
|
||||
if (!CurrentPlatform.isAndroid || !isUnsupportedMimeError(e)) {
|
||||
rethrow;
|
||||
}
|
||||
final id = await _mediaSaveApi.saveToDownloads(filePath, title ?? p.basename(filePath), 'Download/Immich');
|
||||
return id == null ? null : AssetEntity(id: id, typeInt: 1, width: 0, height: 0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<AssetEntity?> saveLivePhoto({required File image, required File video, required String title}) async {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// True when a gallery save failed because MediaStore rejected the file's MIME.
|
||||
// Android raises `Unsupported MIME type` for formats it can't type (e.g. raw
|
||||
// like CR3), where photo_manager falls back to `image/*`. Matched on the detail
|
||||
// string (case-insensitive) because photo_manager surfaces no distinct error
|
||||
// code for it. Keeps `mime type` in the match so it doesn't catch unrelated
|
||||
// `Unsupported*` errors (e.g. UnsupportedOperationException).
|
||||
bool isUnsupportedMimeError(PlatformException e) {
|
||||
final details = e.details;
|
||||
return details is String && details.toLowerCase().contains('unsupported mime type');
|
||||
}
|
||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 3.0.1
|
||||
- API version: 3.0.2
|
||||
- Generator version: 7.22.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:pigeon/pigeon.dart';
|
||||
|
||||
@ConfigurePigeon(
|
||||
PigeonOptions(
|
||||
dartOut: 'lib/platform/media_save_api.g.dart',
|
||||
kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/mediasave/MediaSave.g.kt',
|
||||
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.mediasave'),
|
||||
dartOptions: DartOptions(),
|
||||
dartPackageName: 'immich_mobile',
|
||||
),
|
||||
)
|
||||
@HostApi()
|
||||
abstract class MediaSaveApi {
|
||||
// Saves a file to a MediaStore Files-collection entry under [relativePath]
|
||||
// (e.g. Download/Immich). Fallback for when photo_manager can't save a file
|
||||
// because the platform has no MIME for it (e.g. raw like CR3) and MediaStore
|
||||
// rejects the `image/*` it falls back to; the Files collection accepts any
|
||||
// type. Returns the new media id, or null on failure.
|
||||
@async
|
||||
String? saveToDownloads(String filePath, String title, String? relativePath);
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.0.1+3054
|
||||
version: 3.0.2+3055
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/mime.utils.dart';
|
||||
|
||||
void main() {
|
||||
group('isUnsupportedMimeError', () {
|
||||
PlatformException err(Object? details) => PlatformException(code: 'saveImageWithPath', details: details);
|
||||
|
||||
test('detects the MediaStore unsupported-mime failure', () {
|
||||
expect(isUnsupportedMimeError(err('java.lang.IllegalArgumentException: Unsupported MIME type image/*')), isTrue);
|
||||
});
|
||||
|
||||
test('is case-insensitive', () {
|
||||
expect(isUnsupportedMimeError(err('unsupported mime type foo')), isTrue);
|
||||
});
|
||||
|
||||
test('ignores other Unsupported* errors', () {
|
||||
expect(isUnsupportedMimeError(err('java.lang.UnsupportedOperationException')), isFalse);
|
||||
});
|
||||
|
||||
test('ignores unrelated failures and non-string details', () {
|
||||
expect(isUnsupportedMimeError(err('Permission denied')), isFalse);
|
||||
expect(isUnsupportedMimeError(err(null)), isFalse);
|
||||
expect(isUnsupportedMimeError(err(42)), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -16223,7 +16223,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "Monorepo for Immich",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 3.0.1
|
||||
* 3.0.2
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user