mirror of
https://github.com/immich-app/immich.git
synced 2026-07-07 13:07:04 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66430b3dd7 |
+1
-1
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.100.0"
|
||||
wrangler = "4.107.0"
|
||||
|
||||
@@ -287,43 +287,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "11.6.0"
|
||||
version = "11.9.0"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64"]
|
||||
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120174"
|
||||
checksum = "sha256:7981d0109fffdf8f15b838243240c8c9f9114dd4b7111d4227c1453522d75b61"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748044"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120177"
|
||||
checksum = "sha256:3622d3010d6af601190e7b63994fb0fe297e805699145aec949f4879f2672fb5"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748040"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64"]
|
||||
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120176"
|
||||
checksum = "sha256:8d987d82585453bcf260ea5d0bae346d298f1a5e71bcde8d99976521408ad895"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748039"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120175"
|
||||
checksum = "sha256:655fcf2609e3145cfd7c2f05113429523706d0a618f2957eaa6c3562f6a5998b"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748037"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-arm64"]
|
||||
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120180"
|
||||
checksum = "sha256:ca6f98a8911ec975509464026205e2b54aed74cecd36c5ae9932e5997bd7de2a"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748038"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.windows-x64"]
|
||||
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120173"
|
||||
checksum = "sha256:82c130717a59f237c49e8eaa8a7ed75c1d8d351a5b6dedeee3196b8e1b597c6e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748043"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
|
||||
@@ -16,7 +16,7 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
pnpm = "11.6.0"
|
||||
pnpm = "11.9.0"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
|
||||
@@ -70,8 +70,7 @@
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES"
|
||||
queueDebuggingEnabled = "NO">
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
|
||||
@@ -27,6 +27,7 @@ sealed class BaseAsset {
|
||||
final int? height;
|
||||
final int? durationMs;
|
||||
final bool isFavorite;
|
||||
final String? livePhotoVideoId;
|
||||
final bool isEdited;
|
||||
|
||||
const BaseAsset({
|
||||
@@ -39,15 +40,32 @@ sealed class BaseAsset {
|
||||
this.height,
|
||||
this.durationMs,
|
||||
this.isFavorite = false,
|
||||
this.livePhotoVideoId,
|
||||
required this.isEdited,
|
||||
});
|
||||
|
||||
bool get isImage => type == AssetType.image;
|
||||
bool get isVideo => type == AssetType.video;
|
||||
|
||||
bool get isMotionPhoto => playbackStyle == AssetPlaybackStyle.livePhoto;
|
||||
bool get isMotionPhoto => livePhotoVideoId != null;
|
||||
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
|
||||
|
||||
AssetPlaybackStyle get playbackStyle {
|
||||
if (isVideo) {
|
||||
return AssetPlaybackStyle.video;
|
||||
}
|
||||
if (isMotionPhoto) {
|
||||
return AssetPlaybackStyle.livePhoto;
|
||||
}
|
||||
if (isImage && durationMs != null && durationMs! > 0) {
|
||||
return AssetPlaybackStyle.imageAnimated;
|
||||
}
|
||||
if (isImage) {
|
||||
return AssetPlaybackStyle.image;
|
||||
}
|
||||
return AssetPlaybackStyle.unknown;
|
||||
}
|
||||
|
||||
Duration get duration {
|
||||
final durationMs = this.durationMs;
|
||||
if (durationMs != null) {
|
||||
@@ -68,7 +86,6 @@ sealed class BaseAsset {
|
||||
String? get localId;
|
||||
String? get remoteId;
|
||||
String get heroTag;
|
||||
AssetPlaybackStyle get playbackStyle;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
@@ -25,6 +25,7 @@ class LocalAsset extends BaseAsset {
|
||||
super.height,
|
||||
super.durationMs,
|
||||
super.isFavorite = false,
|
||||
super.livePhotoVideoId,
|
||||
this.orientation = 0,
|
||||
required this.playbackStyle,
|
||||
this.adjustmentTime,
|
||||
|
||||
@@ -10,7 +10,6 @@ class RemoteAsset extends BaseAsset {
|
||||
final AssetVisibility visibility;
|
||||
final String ownerId;
|
||||
final String? stackId;
|
||||
final String? livePhotoVideoId;
|
||||
final DateTime? uploadedAt;
|
||||
final DateTime? deletedAt;
|
||||
|
||||
@@ -30,7 +29,7 @@ class RemoteAsset extends BaseAsset {
|
||||
super.isFavorite = false,
|
||||
this.thumbHash,
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
this.livePhotoVideoId,
|
||||
super.livePhotoVideoId,
|
||||
this.stackId,
|
||||
required super.isEdited,
|
||||
this.deletedAt,
|
||||
@@ -39,23 +38,6 @@ class RemoteAsset extends BaseAsset {
|
||||
@override
|
||||
String? get localId => localAssetId;
|
||||
|
||||
@override
|
||||
AssetPlaybackStyle get playbackStyle {
|
||||
if (isVideo) {
|
||||
return AssetPlaybackStyle.video;
|
||||
}
|
||||
if (livePhotoVideoId != null) {
|
||||
return AssetPlaybackStyle.livePhoto;
|
||||
}
|
||||
if (isImage && durationMs != null && durationMs! > 0) {
|
||||
return AssetPlaybackStyle.imageAnimated;
|
||||
}
|
||||
if (isImage) {
|
||||
return AssetPlaybackStyle.image;
|
||||
}
|
||||
return AssetPlaybackStyle.unknown;
|
||||
}
|
||||
|
||||
@override
|
||||
String? get remoteId => id;
|
||||
|
||||
@@ -108,7 +90,6 @@ class RemoteAsset extends BaseAsset {
|
||||
thumbHash == other.thumbHash &&
|
||||
visibility == other.visibility &&
|
||||
stackId == other.stackId &&
|
||||
livePhotoVideoId == other.livePhotoVideoId &&
|
||||
uploadedAt == other.uploadedAt &&
|
||||
deletedAt == other.deletedAt;
|
||||
}
|
||||
@@ -122,7 +103,6 @@ class RemoteAsset extends BaseAsset {
|
||||
thumbHash.hashCode ^
|
||||
visibility.hashCode ^
|
||||
stackId.hashCode ^
|
||||
livePhotoVideoId.hashCode ^
|
||||
uploadedAt.hashCode ^
|
||||
deletedAt.hashCode;
|
||||
|
||||
|
||||
@@ -26,10 +26,6 @@ class AssetService {
|
||||
return _localRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
Future<LocalAsset?> getLocalAsset(String id) {
|
||||
return _localRepository.get(id);
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
||||
return _remoteRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ class StorageRepository {
|
||||
return file;
|
||||
}
|
||||
|
||||
// TODO(agg23): Unify these methods
|
||||
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
||||
File? file;
|
||||
final log = Logger('StorageRepository');
|
||||
|
||||
@@ -114,6 +114,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
||||
_PropertyItem(label: 'Height', value: asset.height?.toString()),
|
||||
_PropertyItem(label: 'Duration', value: asset.durationMs != null ? '${asset.durationMs} ms' : null),
|
||||
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
||||
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
||||
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
|
||||
]);
|
||||
}
|
||||
@@ -150,7 +151,6 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
||||
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
|
||||
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
|
||||
_PropertyItem(label: 'Stack ID', value: asset.stackId),
|
||||
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
||||
];
|
||||
|
||||
properties.insertAll(4, additionalProps);
|
||||
|
||||
@@ -122,14 +122,9 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to retrieve LocalAsset, falling back to remote if it cannot be found
|
||||
final localAsset = await _localPlaybackAsset(videoAsset);
|
||||
|
||||
if (localAsset != null) {
|
||||
final file = localAsset.isMotionPhoto
|
||||
? await StorageRepository().getMotionFileForAsset(localAsset)
|
||||
: await StorageRepository().getFileForAsset(localAsset.id);
|
||||
|
||||
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
||||
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
||||
final file = await StorageRepository().getFileForAsset(id);
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
@@ -146,13 +141,14 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
);
|
||||
}
|
||||
|
||||
final remoteAsset = videoAsset as RemoteAsset;
|
||||
final remoteId = (videoAsset as RemoteAsset).id;
|
||||
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||
final String assetId = remoteAsset.livePhotoVideoId ?? remoteAsset.id;
|
||||
final String videoUrl = '$serverEndpoint/assets/$assetId/$postfixUrl';
|
||||
final String videoUrl = videoAsset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
|
||||
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
|
||||
|
||||
return VideoSource.init(path: videoUrl, type: VideoSourceType.network, headers: ApiService.getRequestHeaders());
|
||||
} catch (error) {
|
||||
@@ -161,43 +157,6 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
||||
}
|
||||
}
|
||||
|
||||
Future<LocalAsset?> _localPlaybackAsset(BaseAsset baseAsset) async {
|
||||
if (!baseAsset.hasLocal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LocalAsset? localAsset;
|
||||
|
||||
if (baseAsset is LocalAsset) {
|
||||
localAsset = baseAsset;
|
||||
} else {
|
||||
final localId = (baseAsset as RemoteAsset).localId;
|
||||
localAsset = localId != null ? await ref.read(assetServiceProvider).getLocalAsset(localId) : null;
|
||||
}
|
||||
|
||||
if (localAsset == null) {
|
||||
_log.severe(
|
||||
'Invariant violation: asset ${baseAsset.name} (${baseAsset.localId}) is marked `hasLocal` but local asset could not be retrieved',
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Clients (local) may not correctly recognize a given asset as a motion photo. This allows for a scenario where both remote and local
|
||||
// have the same asset (hash), but only the remote properly recognizes it as a motion asset
|
||||
// If this scenario occurs, fall back to using the remote asset
|
||||
if (baseAsset.isMotionPhoto && !localAsset.isMotionPhoto) {
|
||||
// Platform mismatch for motion photo, use remote instead
|
||||
_log.warning(
|
||||
'Mismatched local and remote motion states on ${baseAsset.name} (${baseAsset.localId}), local = ${localAsset.isMotionPhoto}, remote = ${baseAsset.isMotionPhoto}',
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return localAsset;
|
||||
}
|
||||
|
||||
void _onPlaybackReady() async {
|
||||
if (!mounted || !widget.isCurrent) {
|
||||
return;
|
||||
|
||||
@@ -297,7 +297,8 @@ class _AssetTypeIcons extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLivePhoto = asset.isMotionPhoto;
|
||||
final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null;
|
||||
final isLivePhoto = remoteAsset?.livePhotoVideoId != null;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@@ -85,7 +85,6 @@ class ApiService {
|
||||
|
||||
// Save in local database for next startup
|
||||
await Store.put(StoreKey.serverEndpoint, endpoint);
|
||||
await updateHeaders();
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
@@ -174,14 +173,8 @@ class ApiService {
|
||||
|
||||
static List<String> getServerUrls() {
|
||||
final urls = <String>[];
|
||||
// Include the original server URL so the native side can use it
|
||||
// to sync the auth token to other external hosts
|
||||
final serverUrl = Store.tryGet(StoreKey.serverUrl);
|
||||
if (serverUrl != null && serverUrl.isNotEmpty) {
|
||||
urls.add(serverUrl);
|
||||
}
|
||||
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint);
|
||||
if (serverEndpoint != null && serverEndpoint.isNotEmpty && serverEndpoint != serverUrl) {
|
||||
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
|
||||
urls.add(serverEndpoint);
|
||||
}
|
||||
final network = SettingsRepository.instance.appConfig.network;
|
||||
|
||||
@@ -70,36 +70,4 @@ void main() {
|
||||
expect((assets.first as RemoteAsset).id, asset.id);
|
||||
});
|
||||
});
|
||||
|
||||
group('live photos', () {
|
||||
test('remote-only live photo contains livePhotoVideoId and is marked as a motion photo', () async {
|
||||
final user = await ctx.newUser();
|
||||
final asset = await ctx.newRemoteAsset(ownerId: user.id, livePhotoVideoId: 'motion-photo-1');
|
||||
|
||||
final assets = await sut.main([user.id], .day).assetSource(0, 10);
|
||||
|
||||
expect(assets, hasLength(1));
|
||||
final remote = assets.single as RemoteAsset;
|
||||
expect(remote.id, asset.id);
|
||||
expect(remote.livePhotoVideoId, 'motion-photo-1');
|
||||
expect(remote.isMotionPhoto, isTrue);
|
||||
expect(remote.localId, isNull);
|
||||
});
|
||||
|
||||
test('merged live photo resolves localId and is marked as a motion photo', () async {
|
||||
final user = await ctx.newUser();
|
||||
const checksum = 'shared-live-photo-checksum';
|
||||
final asset = await ctx.newRemoteAsset(ownerId: user.id, checksum: checksum, livePhotoVideoId: 'motion-photo-2');
|
||||
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||
|
||||
final assets = await sut.main([user.id], .day).assetSource(0, 10);
|
||||
|
||||
expect(assets, hasLength(1));
|
||||
final remote = assets.single as RemoteAsset;
|
||||
expect(remote.id, asset.id);
|
||||
expect(remote.livePhotoVideoId, 'motion-photo-2');
|
||||
expect(remote.isMotionPhoto, isTrue);
|
||||
expect(remote.localId, local.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"format": "prettier --cache --check i18n/",
|
||||
"format:fix": "prettier --cache --write --list-different i18n"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0"
|
||||
"packageManager": "pnpm@11.9.0"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"packageManager": "pnpm@11.6.0",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0"
|
||||
"packageManager": "pnpm@11.9.0"
|
||||
}
|
||||
|
||||
+2
-2
@@ -84,7 +84,7 @@
|
||||
"jose": "^6.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "0.28.17",
|
||||
"kysely": "0.29.2",
|
||||
"kysely-postgres-js": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.2",
|
||||
@@ -116,7 +116,7 @@
|
||||
"ua-parser-js": "^2.0.0",
|
||||
"uuid": "^14.0.0",
|
||||
"validator": "^13.12.0",
|
||||
"zod": "4.3.6"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.0",
|
||||
|
||||
+2
-2
@@ -76,7 +76,7 @@
|
||||
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
|
||||
"@socket.io/component-emitter": "^3.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/enhanced-img": "^0.10.4",
|
||||
"@sveltejs/enhanced-img": "^0.11.0",
|
||||
"@sveltejs/kit": "^2.56.1",
|
||||
"@sveltejs/vite-plugin-svelte": "7.1.2",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
@@ -105,7 +105,7 @@
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"prettier-plugin-svelte": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"svelte": "5.56.3",
|
||||
"svelte": "5.56.4",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.2.4",
|
||||
|
||||
Reference in New Issue
Block a user