mirror of
https://github.com/immich-app/immich.git
synced 2026-05-29 19:12:32 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b15a90618 |
@@ -1,46 +1,46 @@
|
|||||||
dev:
|
dev:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
dev-down:
|
dev-down:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1
|
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||||
|
|
||||||
dev-update:
|
dev-update:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1
|
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
dev-docs:
|
dev-docs:
|
||||||
npm --prefix docs run start
|
npm --prefix docs run start
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
||||||
|
|
||||||
e2e-dev:
|
e2e-dev:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.dev.yml up --remove-orphans
|
||||||
|
|
||||||
e2e-update:
|
e2e-update:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1
|
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
e2e-down:
|
e2e-down:
|
||||||
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1
|
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-down:
|
prod-down:
|
||||||
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1
|
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1
|
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
.PHONY: open-api
|
.PHONY: open-api
|
||||||
open-api:
|
open-api:
|
||||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1
|
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
|
||||||
|
|
||||||
sql:
|
sql:
|
||||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1
|
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
|
||||||
|
|
||||||
|
|
||||||
renovate:
|
renovate:
|
||||||
@@ -52,7 +52,16 @@ renovate:
|
|||||||
MODULES = e2e server web cli sdk docs .github
|
MODULES = e2e server web cli sdk docs .github
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
@printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1
|
docker compose -f ./e2e/docker-compose.yml build
|
||||||
|
pnpm --filter immich-e2e run test
|
||||||
|
pnpm --filter immich-e2e run test:web
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1
|
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||||
|
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||||
|
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||||
|
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||||
|
find . -name "coverage" -type d -prune -exec rm -rf '{}' +
|
||||||
|
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
|
||||||
|
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
|
||||||
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
|
||||||
|
|||||||
+1
-16
@@ -1,21 +1,11 @@
|
|||||||
[tasks.install]
|
[tasks.install]
|
||||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||||
|
|
||||||
[tasks.build]
|
|
||||||
dir = "{{ config_root }}"
|
|
||||||
run = "docker compose build"
|
|
||||||
|
|
||||||
[tasks.test]
|
[tasks.test]
|
||||||
depends = ["//e2e:build", "//e2e:ci-setup"]
|
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
run = "vitest --run"
|
run = "vitest --run"
|
||||||
|
|
||||||
[tasks.playwright-install]
|
|
||||||
env._.path = "./node_modules/.bin"
|
|
||||||
run = "playwright install"
|
|
||||||
|
|
||||||
[tasks."test-web"]
|
[tasks."test-web"]
|
||||||
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
|
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
run = "playwright test"
|
run = "playwright test"
|
||||||
|
|
||||||
@@ -40,12 +30,7 @@ run = "tsc --noEmit"
|
|||||||
|
|
||||||
|
|
||||||
[tasks.ci-setup]
|
[tasks.ci-setup]
|
||||||
depends = [
|
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
|
||||||
"//:sdk:install",
|
|
||||||
"//:sdk:build",
|
|
||||||
"//packages/cli:install",
|
|
||||||
"//packages/cli:build",
|
|
||||||
]
|
|
||||||
run = { task = ":install" }
|
run = { task = ":install" }
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ describe('/server', () => {
|
|||||||
major: expect.any(Number),
|
major: expect.any(Number),
|
||||||
minor: expect.any(Number),
|
minor: expect.any(Number),
|
||||||
patch: expect.any(Number),
|
patch: expect.any(Number),
|
||||||
prerelease: null,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ describe('/system-config', () => {
|
|||||||
const response1 = await request(app)
|
const response1 = await request(app)
|
||||||
.put('/system-config')
|
.put('/system-config')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
.send({ ...config, newVersionCheck: { enabled: false } });
|
||||||
|
|
||||||
expect(response1.status).toBe(200);
|
expect(response1.status).toBe(200);
|
||||||
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } });
|
||||||
|
|
||||||
const response2 = await request(app)
|
const response2 = await request(app)
|
||||||
.put('/system-config')
|
.put('/system-config')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
.send({ ...config, newVersionCheck: { enabled: true } });
|
||||||
|
|
||||||
expect(response2.status).toBe(200);
|
expect(response2.status).toBe(200);
|
||||||
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid config entry', async () => {
|
it('should reject an invalid config entry', async () => {
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
|
|||||||
result.duration.push(asset.duration);
|
result.duration.push(asset.duration);
|
||||||
result.projectionType.push(asset.projectionType);
|
result.projectionType.push(asset.projectionType);
|
||||||
result.livePhotoVideoId.push(asset.livePhotoVideoId);
|
result.livePhotoVideoId.push(asset.livePhotoVideoId);
|
||||||
result.city?.push(asset.city);
|
result.city.push(asset.city);
|
||||||
result.country?.push(asset.country);
|
result.country.push(asset.country);
|
||||||
result.visibility.push(asset.visibility);
|
result.visibility.push(asset.visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,8 +305,6 @@
|
|||||||
"refreshing_all_libraries": "Refreshing all libraries",
|
"refreshing_all_libraries": "Refreshing all libraries",
|
||||||
"registration": "Admin Registration",
|
"registration": "Admin Registration",
|
||||||
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
||||||
"release_channel_release_candidate": "Release candidate",
|
|
||||||
"release_channel_stable": "Stable",
|
|
||||||
"remove_failed_jobs": "Remove failed jobs",
|
"remove_failed_jobs": "Remove failed jobs",
|
||||||
"require_password_change_on_login": "Require user to change password on first login",
|
"require_password_change_on_login": "Require user to change password on first login",
|
||||||
"reset_settings_to_default": "Reset settings to default",
|
"reset_settings_to_default": "Reset settings to default",
|
||||||
@@ -444,8 +442,6 @@
|
|||||||
"user_settings_description": "Manage user settings",
|
"user_settings_description": "Manage user settings",
|
||||||
"user_successfully_removed": "User {email} has been successfully removed.",
|
"user_successfully_removed": "User {email} has been successfully removed.",
|
||||||
"users_page_description": "Admin users page",
|
"users_page_description": "Admin users page",
|
||||||
"version_check_channel": "Release channel",
|
|
||||||
"version_check_channel_description": "Pick the release channel you want to get version announcements for",
|
|
||||||
"version_check_enabled_description": "Enable version check",
|
"version_check_enabled_description": "Enable version check",
|
||||||
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
||||||
"version_check_settings": "Version Check",
|
"version_check_settings": "Version Check",
|
||||||
|
|||||||
@@ -84,72 +84,6 @@ run = [
|
|||||||
dir = "server"
|
dir = "server"
|
||||||
run = "node ./dist/bin/sync-sql.js"
|
run = "node ./dist/bin/sync-sql.js"
|
||||||
|
|
||||||
# TODO dev, prod, and e2e should be de-duplicated by using env but for some reason I ran into issues
|
|
||||||
[tasks.dev]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "docker"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
|
|
||||||
depends_post = "//:dev-down"
|
|
||||||
|
|
||||||
[tasks.dev-update]
|
|
||||||
run = { task = "//:dev", args = ["--build", "-V"] }
|
|
||||||
|
|
||||||
[tasks.dev-scale]
|
|
||||||
run = { task = "//:dev", args = ["--build", "-V", "--scale immich-server=3"] }
|
|
||||||
|
|
||||||
[tasks.dev-down]
|
|
||||||
dir = "docker"
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
|
|
||||||
|
|
||||||
[tasks.prod]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "docker"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.prod.yml up --remove-orphans"
|
|
||||||
depends_post = "//:prod-down"
|
|
||||||
|
|
||||||
[tasks.prod-scale]
|
|
||||||
run = { task = "//:prod", args = [
|
|
||||||
"--build",
|
|
||||||
"-V",
|
|
||||||
"--scale immich-server=3",
|
|
||||||
"--scale immich-microservices",
|
|
||||||
] }
|
|
||||||
|
|
||||||
[tasks.prod-down]
|
|
||||||
dir = "docker"
|
|
||||||
run = "docker compose -f ./docker-compose.prod.yml down --remove-orphans"
|
|
||||||
|
|
||||||
[tasks.e2e]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "e2e"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.yml up --remove-orphans"
|
|
||||||
depends_post = "//:e2e-down"
|
|
||||||
|
|
||||||
[tasks.e2e-dev]
|
|
||||||
depends = "//:plugins"
|
|
||||||
dir = "e2e"
|
|
||||||
interactive = true
|
|
||||||
env = { COMPOSE_BAKE = true }
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
|
|
||||||
depends_post = "//:e2e-dev-down"
|
|
||||||
|
|
||||||
[tasks.e2e-update]
|
|
||||||
run = { task = "//:e2e", args = ["--build", '-V'] }
|
|
||||||
|
|
||||||
[tasks.e2e-down]
|
|
||||||
dir = "e2e"
|
|
||||||
run = "docker compose -f ./docker-compose.yml down --remove-orphans"
|
|
||||||
|
|
||||||
[tasks.e2e-dev-down]
|
|
||||||
dir = "e2e"
|
|
||||||
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
|
|
||||||
|
|
||||||
# SDK tasks
|
# SDK tasks
|
||||||
[tasks."sdk:install"]
|
[tasks."sdk:install"]
|
||||||
dir = "packages/sdk"
|
dir = "packages/sdk"
|
||||||
@@ -165,14 +99,3 @@ run = "pnpm format"
|
|||||||
|
|
||||||
[tasks."i18n:format-fix"]
|
[tasks."i18n:format-fix"]
|
||||||
run = "pnpm format:fix"
|
run = "pnpm format:fix"
|
||||||
|
|
||||||
[tasks.clean]
|
|
||||||
run = [
|
|
||||||
"find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name 'dist' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name 'build' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name '.svelte-kit' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name 'coverage' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
"find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +",
|
|
||||||
{ task = "//:*-down" },
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import 'package:immich_mobile/utils/semver.dart';
|
|||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ServerVersion extends SemVer {
|
class ServerVersion extends SemVer {
|
||||||
const ServerVersion({required super.major, required super.minor, required super.patch, super.prerelease});
|
const ServerVersion({required super.major, required super.minor, required super.patch});
|
||||||
|
|
||||||
ServerVersion.fromDto(ServerVersionResponseDto dto)
|
@override
|
||||||
: super(major: dto.major, minor: dto.minor, patch: dto.patch_, prerelease: dto.prerelease);
|
String toString() {
|
||||||
|
return 'ServerVersion(major: $major, minor: $minor, patch: $patch)';
|
||||||
|
}
|
||||||
|
|
||||||
bool isAtLeast({int major = 0, int minor = 0, int patch = 0, int? prerelease}) {
|
ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_);
|
||||||
return this >= SemVer(major: major, minor: minor, patch: patch, prerelease: prerelease);
|
|
||||||
|
bool isAtLeast({int major = 0, int minor = 0, int patch = 0}) {
|
||||||
|
return this >= SemVer(major: major, minor: minor, patch: patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,36 @@
|
|||||||
enum SemVerType { major, minor, patch, prerelease }
|
enum SemVerType { major, minor, patch }
|
||||||
|
|
||||||
class SemVer {
|
class SemVer {
|
||||||
final int major;
|
final int major;
|
||||||
final int minor;
|
final int minor;
|
||||||
final int patch;
|
final int patch;
|
||||||
final int? prerelease;
|
|
||||||
|
|
||||||
const SemVer({required this.major, required this.minor, required this.patch, this.prerelease});
|
const SemVer({required this.major, required this.minor, required this.patch});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '$major.$minor.$patch${prerelease == null ? '' : '-rc.$prerelease'}';
|
return '$major.$minor.$patch';
|
||||||
}
|
}
|
||||||
|
|
||||||
SemVer copyWith({int? major, int? minor, int? patch, int? prerelease}) {
|
SemVer copyWith({int? major, int? minor, int? patch}) {
|
||||||
return SemVer(
|
return SemVer(major: major ?? this.major, minor: minor ?? this.minor, patch: patch ?? this.patch);
|
||||||
major: major ?? this.major,
|
|
||||||
minor: minor ?? this.minor,
|
|
||||||
patch: patch ?? this.patch,
|
|
||||||
prerelease: prerelease ?? this.prerelease,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static final _pattern = RegExp(r'^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?(?:[-+].*)?$', caseSensitive: false);
|
|
||||||
|
|
||||||
factory SemVer.fromString(String version) {
|
factory SemVer.fromString(String version) {
|
||||||
final match = _pattern.firstMatch(version);
|
if (version.toLowerCase().startsWith("v")) {
|
||||||
if (match == null) {
|
version = version.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final parts = version.split("-")[0].split('.');
|
||||||
|
if (parts.length != 3) {
|
||||||
throw FormatException('Invalid semantic version string: $version');
|
throw FormatException('Invalid semantic version string: $version');
|
||||||
}
|
}
|
||||||
|
|
||||||
final prerelease = match.group(4);
|
try {
|
||||||
return SemVer(
|
return SemVer(major: int.parse(parts[0]), minor: int.parse(parts[1]), patch: int.parse(parts[2]));
|
||||||
major: int.parse(match.group(1)!),
|
} catch (e) {
|
||||||
minor: int.parse(match.group(2)!),
|
throw FormatException('Invalid semantic version string: $version');
|
||||||
patch: int.parse(match.group(3)!),
|
}
|
||||||
prerelease: prerelease == null ? null : int.parse(prerelease),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator >(SemVer other) {
|
bool operator >(SemVer other) {
|
||||||
@@ -46,10 +40,7 @@ class SemVer {
|
|||||||
if (minor != other.minor) {
|
if (minor != other.minor) {
|
||||||
return minor > other.minor;
|
return minor > other.minor;
|
||||||
}
|
}
|
||||||
if (patch != other.patch) {
|
return patch > other.patch;
|
||||||
return patch > other.patch;
|
|
||||||
}
|
|
||||||
return _comparePrerelease(other) > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator <(SemVer other) {
|
bool operator <(SemVer other) {
|
||||||
@@ -59,23 +50,7 @@ class SemVer {
|
|||||||
if (minor != other.minor) {
|
if (minor != other.minor) {
|
||||||
return minor < other.minor;
|
return minor < other.minor;
|
||||||
}
|
}
|
||||||
if (patch != other.patch) {
|
return patch < other.patch;
|
||||||
return patch < other.patch;
|
|
||||||
}
|
|
||||||
return _comparePrerelease(other) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int _comparePrerelease(SemVer other) {
|
|
||||||
if (prerelease == other.prerelease) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (prerelease == null) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (other.prerelease == null) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return prerelease!.compareTo(other.prerelease!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator >=(SemVer other) {
|
bool operator >=(SemVer other) {
|
||||||
@@ -92,11 +67,7 @@ class SemVer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return other is SemVer &&
|
return other is SemVer && other.major == major && other.minor == minor && other.patch == patch;
|
||||||
other.major == major &&
|
|
||||||
other.minor == minor &&
|
|
||||||
other.patch == patch &&
|
|
||||||
other.prerelease == prerelease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SemVerType? differenceType(SemVer other) {
|
SemVerType? differenceType(SemVer other) {
|
||||||
@@ -109,13 +80,10 @@ class SemVer {
|
|||||||
if (patch != other.patch) {
|
if (patch != other.patch) {
|
||||||
return SemVerType.patch;
|
return SemVerType.patch;
|
||||||
}
|
}
|
||||||
if (prerelease != other.prerelease) {
|
|
||||||
return SemVerType.prerelease;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode ^ prerelease.hashCode;
|
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ class AppBarServerInfo extends HookConsumerWidget {
|
|||||||
divider,
|
divider,
|
||||||
_ServerInfoItem(
|
_ServerInfoItem(
|
||||||
label: "server_version".tr(),
|
label: "server_version".tr(),
|
||||||
text: serverInfoState.serverVersion.major > 0 ? "${serverInfoState.serverVersion}" : "--",
|
text: serverInfoState.serverVersion.major > 0
|
||||||
|
? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
|
||||||
|
: "--",
|
||||||
),
|
),
|
||||||
divider,
|
divider,
|
||||||
_ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true),
|
_ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true),
|
||||||
@@ -58,7 +60,9 @@ class AppBarServerInfo extends HookConsumerWidget {
|
|||||||
divider,
|
divider,
|
||||||
_ServerInfoItem(
|
_ServerInfoItem(
|
||||||
label: "latest_version".tr(),
|
label: "latest_version".tr(),
|
||||||
text: serverInfoState.latestVersion!.major > 0 ? "${serverInfoState.latestVersion!}" : "--",
|
text: serverInfoState.latestVersion!.major > 0
|
||||||
|
? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}"
|
||||||
|
: "--",
|
||||||
tooltip: true,
|
tooltip: true,
|
||||||
icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate
|
icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate
|
||||||
? const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12)
|
? const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12)
|
||||||
|
|||||||
Generated
-3
@@ -513,9 +513,6 @@ Class | Method | HTTP request | Description
|
|||||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||||
- [ReactionLevel](doc//ReactionLevel.md)
|
- [ReactionLevel](doc//ReactionLevel.md)
|
||||||
- [ReactionType](doc//ReactionType.md)
|
- [ReactionType](doc//ReactionType.md)
|
||||||
- [ReleaseChannel](doc//ReleaseChannel.md)
|
|
||||||
- [ReleaseEventV1](doc//ReleaseEventV1.md)
|
|
||||||
- [ReleaseType](doc//ReleaseType.md)
|
|
||||||
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
||||||
- [RotateParameters](doc//RotateParameters.md)
|
- [RotateParameters](doc//RotateParameters.md)
|
||||||
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
||||||
|
|||||||
Generated
-3
@@ -258,9 +258,6 @@ part 'model/ratings_response.dart';
|
|||||||
part 'model/ratings_update.dart';
|
part 'model/ratings_update.dart';
|
||||||
part 'model/reaction_level.dart';
|
part 'model/reaction_level.dart';
|
||||||
part 'model/reaction_type.dart';
|
part 'model/reaction_type.dart';
|
||||||
part 'model/release_channel.dart';
|
|
||||||
part 'model/release_event_v1.dart';
|
|
||||||
part 'model/release_type.dart';
|
|
||||||
part 'model/reverse_geocoding_state_response_dto.dart';
|
part 'model/reverse_geocoding_state_response_dto.dart';
|
||||||
part 'model/rotate_parameters.dart';
|
part 'model/rotate_parameters.dart';
|
||||||
part 'model/search_album_response_dto.dart';
|
part 'model/search_album_response_dto.dart';
|
||||||
|
|||||||
Generated
-6
@@ -562,12 +562,6 @@ class ApiClient {
|
|||||||
return ReactionLevelTypeTransformer().decode(value);
|
return ReactionLevelTypeTransformer().decode(value);
|
||||||
case 'ReactionType':
|
case 'ReactionType':
|
||||||
return ReactionTypeTypeTransformer().decode(value);
|
return ReactionTypeTypeTransformer().decode(value);
|
||||||
case 'ReleaseChannel':
|
|
||||||
return ReleaseChannelTypeTransformer().decode(value);
|
|
||||||
case 'ReleaseEventV1':
|
|
||||||
return ReleaseEventV1.fromJson(value);
|
|
||||||
case 'ReleaseType':
|
|
||||||
return ReleaseTypeTypeTransformer().decode(value);
|
|
||||||
case 'ReverseGeocodingStateResponseDto':
|
case 'ReverseGeocodingStateResponseDto':
|
||||||
return ReverseGeocodingStateResponseDto.fromJson(value);
|
return ReverseGeocodingStateResponseDto.fromJson(value);
|
||||||
case 'RotateParameters':
|
case 'RotateParameters':
|
||||||
|
|||||||
Generated
-6
@@ -157,12 +157,6 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is ReactionType) {
|
if (value is ReactionType) {
|
||||||
return ReactionTypeTypeTransformer().encode(value).toString();
|
return ReactionTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
if (value is ReleaseChannel) {
|
|
||||||
return ReleaseChannelTypeTransformer().encode(value).toString();
|
|
||||||
}
|
|
||||||
if (value is ReleaseType) {
|
|
||||||
return ReleaseTypeTypeTransformer().encode(value).toString();
|
|
||||||
}
|
|
||||||
if (value is SearchSuggestionType) {
|
if (value is SearchSuggestionType) {
|
||||||
return SearchSuggestionTypeTypeTransformer().encode(value).toString();
|
return SearchSuggestionTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
//
|
|
||||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
|
||||||
//
|
|
||||||
// @dart=2.18
|
|
||||||
|
|
||||||
// ignore_for_file: unused_element, unused_import
|
|
||||||
// ignore_for_file: always_put_required_named_parameters_first
|
|
||||||
// ignore_for_file: constant_identifier_names
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
part of openapi.api;
|
|
||||||
|
|
||||||
/// Release channel
|
|
||||||
class ReleaseChannel {
|
|
||||||
/// Instantiate a new enum with the provided [value].
|
|
||||||
const ReleaseChannel._(this.value);
|
|
||||||
|
|
||||||
/// The underlying value of this enum member.
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => value;
|
|
||||||
|
|
||||||
String toJson() => value;
|
|
||||||
|
|
||||||
static const stable = ReleaseChannel._(r'stable');
|
|
||||||
static const releaseCandidate = ReleaseChannel._(r'releaseCandidate');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][ReleaseChannel].
|
|
||||||
static const values = <ReleaseChannel>[
|
|
||||||
stable,
|
|
||||||
releaseCandidate,
|
|
||||||
];
|
|
||||||
|
|
||||||
static ReleaseChannel? fromJson(dynamic value) => ReleaseChannelTypeTransformer().decode(value);
|
|
||||||
|
|
||||||
static List<ReleaseChannel> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <ReleaseChannel>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = ReleaseChannel.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transformation class that can [encode] an instance of [ReleaseChannel] to String,
|
|
||||||
/// and [decode] dynamic data back to [ReleaseChannel].
|
|
||||||
class ReleaseChannelTypeTransformer {
|
|
||||||
factory ReleaseChannelTypeTransformer() => _instance ??= const ReleaseChannelTypeTransformer._();
|
|
||||||
|
|
||||||
const ReleaseChannelTypeTransformer._();
|
|
||||||
|
|
||||||
String encode(ReleaseChannel data) => data.value;
|
|
||||||
|
|
||||||
/// Decodes a [dynamic value][data] to a ReleaseChannel.
|
|
||||||
///
|
|
||||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
|
||||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
|
||||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
|
||||||
///
|
|
||||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
|
||||||
/// and users are still using an old app with the old code.
|
|
||||||
ReleaseChannel? decode(dynamic data, {bool allowNull = true}) {
|
|
||||||
if (data != null) {
|
|
||||||
switch (data) {
|
|
||||||
case r'stable': return ReleaseChannel.stable;
|
|
||||||
case r'releaseCandidate': return ReleaseChannel.releaseCandidate;
|
|
||||||
default:
|
|
||||||
if (!allowNull) {
|
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Singleton [ReleaseChannelTypeTransformer] instance.
|
|
||||||
static ReleaseChannelTypeTransformer? _instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
-133
@@ -1,133 +0,0 @@
|
|||||||
//
|
|
||||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
|
||||||
//
|
|
||||||
// @dart=2.18
|
|
||||||
|
|
||||||
// ignore_for_file: unused_element, unused_import
|
|
||||||
// ignore_for_file: always_put_required_named_parameters_first
|
|
||||||
// ignore_for_file: constant_identifier_names
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
part of openapi.api;
|
|
||||||
|
|
||||||
class ReleaseEventV1 {
|
|
||||||
/// Returns a new [ReleaseEventV1] instance.
|
|
||||||
ReleaseEventV1({
|
|
||||||
required this.checkedAt,
|
|
||||||
required this.isAvailable,
|
|
||||||
required this.releaseVersion,
|
|
||||||
required this.serverVersion,
|
|
||||||
required this.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// When the server last checked for a latest version. As an ISO timestamp
|
|
||||||
String checkedAt;
|
|
||||||
|
|
||||||
/// Whether a new version is available
|
|
||||||
bool isAvailable;
|
|
||||||
|
|
||||||
ServerVersionResponseDto releaseVersion;
|
|
||||||
|
|
||||||
ServerVersionResponseDto serverVersion;
|
|
||||||
|
|
||||||
ReleaseType type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is ReleaseEventV1 &&
|
|
||||||
other.checkedAt == checkedAt &&
|
|
||||||
other.isAvailable == isAvailable &&
|
|
||||||
other.releaseVersion == releaseVersion &&
|
|
||||||
other.serverVersion == serverVersion &&
|
|
||||||
other.type == type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(checkedAt.hashCode) +
|
|
||||||
(isAvailable.hashCode) +
|
|
||||||
(releaseVersion.hashCode) +
|
|
||||||
(serverVersion.hashCode) +
|
|
||||||
(type.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'ReleaseEventV1[checkedAt=$checkedAt, isAvailable=$isAvailable, releaseVersion=$releaseVersion, serverVersion=$serverVersion, type=$type]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json[r'checkedAt'] = this.checkedAt;
|
|
||||||
json[r'isAvailable'] = this.isAvailable;
|
|
||||||
json[r'releaseVersion'] = this.releaseVersion;
|
|
||||||
json[r'serverVersion'] = this.serverVersion;
|
|
||||||
json[r'type'] = this.type;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [ReleaseEventV1] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static ReleaseEventV1? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "ReleaseEventV1");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return ReleaseEventV1(
|
|
||||||
checkedAt: mapValueOfType<String>(json, r'checkedAt')!,
|
|
||||||
isAvailable: mapValueOfType<bool>(json, r'isAvailable')!,
|
|
||||||
releaseVersion: ServerVersionResponseDto.fromJson(json[r'releaseVersion'])!,
|
|
||||||
serverVersion: ServerVersionResponseDto.fromJson(json[r'serverVersion'])!,
|
|
||||||
type: ReleaseType.fromJson(json[r'type'])!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<ReleaseEventV1> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <ReleaseEventV1>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = ReleaseEventV1.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, ReleaseEventV1> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, ReleaseEventV1>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = ReleaseEventV1.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of ReleaseEventV1-objects as value to a dart map
|
|
||||||
static Map<String, List<ReleaseEventV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<ReleaseEventV1>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = ReleaseEventV1.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'checkedAt',
|
|
||||||
'isAvailable',
|
|
||||||
'releaseVersion',
|
|
||||||
'serverVersion',
|
|
||||||
'type',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
//
|
|
||||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
|
||||||
//
|
|
||||||
// @dart=2.18
|
|
||||||
|
|
||||||
// ignore_for_file: unused_element, unused_import
|
|
||||||
// ignore_for_file: always_put_required_named_parameters_first
|
|
||||||
// ignore_for_file: constant_identifier_names
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
part of openapi.api;
|
|
||||||
|
|
||||||
|
|
||||||
class ReleaseType {
|
|
||||||
/// Instantiate a new enum with the provided [value].
|
|
||||||
const ReleaseType._(this.value);
|
|
||||||
|
|
||||||
/// The underlying value of this enum member.
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => value;
|
|
||||||
|
|
||||||
String toJson() => value;
|
|
||||||
|
|
||||||
static const major = ReleaseType._(r'major');
|
|
||||||
static const premajor = ReleaseType._(r'premajor');
|
|
||||||
static const minor = ReleaseType._(r'minor');
|
|
||||||
static const preminor = ReleaseType._(r'preminor');
|
|
||||||
static const patch_ = ReleaseType._(r'patch');
|
|
||||||
static const prepatch = ReleaseType._(r'prepatch');
|
|
||||||
static const prerelease = ReleaseType._(r'prerelease');
|
|
||||||
static const release = ReleaseType._(r'release');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][ReleaseType].
|
|
||||||
static const values = <ReleaseType>[
|
|
||||||
major,
|
|
||||||
premajor,
|
|
||||||
minor,
|
|
||||||
preminor,
|
|
||||||
patch_,
|
|
||||||
prepatch,
|
|
||||||
prerelease,
|
|
||||||
release,
|
|
||||||
];
|
|
||||||
|
|
||||||
static ReleaseType? fromJson(dynamic value) => ReleaseTypeTypeTransformer().decode(value);
|
|
||||||
|
|
||||||
static List<ReleaseType> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <ReleaseType>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = ReleaseType.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transformation class that can [encode] an instance of [ReleaseType] to String,
|
|
||||||
/// and [decode] dynamic data back to [ReleaseType].
|
|
||||||
class ReleaseTypeTypeTransformer {
|
|
||||||
factory ReleaseTypeTypeTransformer() => _instance ??= const ReleaseTypeTypeTransformer._();
|
|
||||||
|
|
||||||
const ReleaseTypeTypeTransformer._();
|
|
||||||
|
|
||||||
String encode(ReleaseType data) => data.value;
|
|
||||||
|
|
||||||
/// Decodes a [dynamic value][data] to a ReleaseType.
|
|
||||||
///
|
|
||||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
|
||||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
|
||||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
|
||||||
///
|
|
||||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
|
||||||
/// and users are still using an old app with the old code.
|
|
||||||
ReleaseType? decode(dynamic data, {bool allowNull = true}) {
|
|
||||||
if (data != null) {
|
|
||||||
switch (data) {
|
|
||||||
case r'major': return ReleaseType.major;
|
|
||||||
case r'premajor': return ReleaseType.premajor;
|
|
||||||
case r'minor': return ReleaseType.minor;
|
|
||||||
case r'preminor': return ReleaseType.preminor;
|
|
||||||
case r'patch': return ReleaseType.patch_;
|
|
||||||
case r'prepatch': return ReleaseType.prepatch;
|
|
||||||
case r'prerelease': return ReleaseType.prerelease;
|
|
||||||
case r'release': return ReleaseType.release;
|
|
||||||
default:
|
|
||||||
if (!allowNull) {
|
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Singleton [ReleaseTypeTypeTransformer] instance.
|
|
||||||
static ReleaseTypeTypeTransformer? _instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
+6
-22
@@ -16,61 +16,47 @@ class ServerVersionResponseDto {
|
|||||||
required this.major,
|
required this.major,
|
||||||
required this.minor,
|
required this.minor,
|
||||||
required this.patch_,
|
required this.patch_,
|
||||||
required this.prerelease,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Major version number
|
/// Major version number
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: -9007199254740991
|
||||||
/// Maximum value: 9007199254740991
|
/// Maximum value: 9007199254740991
|
||||||
int major;
|
int major;
|
||||||
|
|
||||||
/// Minor version number
|
/// Minor version number
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: -9007199254740991
|
||||||
/// Maximum value: 9007199254740991
|
/// Maximum value: 9007199254740991
|
||||||
int minor;
|
int minor;
|
||||||
|
|
||||||
/// Patch version number
|
/// Patch version number
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: -9007199254740991
|
||||||
/// Maximum value: 9007199254740991
|
/// Maximum value: 9007199254740991
|
||||||
int patch_;
|
int patch_;
|
||||||
|
|
||||||
/// Pre-release version number
|
|
||||||
///
|
|
||||||
/// Minimum value: 0
|
|
||||||
/// Maximum value: 9007199254740991
|
|
||||||
int? prerelease;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is ServerVersionResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is ServerVersionResponseDto &&
|
||||||
other.major == major &&
|
other.major == major &&
|
||||||
other.minor == minor &&
|
other.minor == minor &&
|
||||||
other.patch_ == patch_ &&
|
other.patch_ == patch_;
|
||||||
other.prerelease == prerelease;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(major.hashCode) +
|
(major.hashCode) +
|
||||||
(minor.hashCode) +
|
(minor.hashCode) +
|
||||||
(patch_.hashCode) +
|
(patch_.hashCode);
|
||||||
(prerelease == null ? 0 : prerelease!.hashCode);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_, prerelease=$prerelease]';
|
String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'major'] = this.major;
|
json[r'major'] = this.major;
|
||||||
json[r'minor'] = this.minor;
|
json[r'minor'] = this.minor;
|
||||||
json[r'patch'] = this.patch_;
|
json[r'patch'] = this.patch_;
|
||||||
if (this.prerelease != null) {
|
|
||||||
json[r'prerelease'] = this.prerelease;
|
|
||||||
} else {
|
|
||||||
// json[r'prerelease'] = null;
|
|
||||||
}
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +72,6 @@ class ServerVersionResponseDto {
|
|||||||
major: mapValueOfType<int>(json, r'major')!,
|
major: mapValueOfType<int>(json, r'major')!,
|
||||||
minor: mapValueOfType<int>(json, r'minor')!,
|
minor: mapValueOfType<int>(json, r'minor')!,
|
||||||
patch_: mapValueOfType<int>(json, r'patch')!,
|
patch_: mapValueOfType<int>(json, r'patch')!,
|
||||||
prerelease: mapValueOfType<int>(json, r'prerelease'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -137,7 +122,6 @@ class ServerVersionResponseDto {
|
|||||||
'major',
|
'major',
|
||||||
'minor',
|
'minor',
|
||||||
'patch',
|
'patch',
|
||||||
'prerelease',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,32 +13,26 @@ part of openapi.api;
|
|||||||
class SystemConfigNewVersionCheckDto {
|
class SystemConfigNewVersionCheckDto {
|
||||||
/// Returns a new [SystemConfigNewVersionCheckDto] instance.
|
/// Returns a new [SystemConfigNewVersionCheckDto] instance.
|
||||||
SystemConfigNewVersionCheckDto({
|
SystemConfigNewVersionCheckDto({
|
||||||
required this.channel,
|
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
ReleaseChannel channel;
|
|
||||||
|
|
||||||
/// Enabled
|
/// Enabled
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigNewVersionCheckDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigNewVersionCheckDto &&
|
||||||
other.channel == channel &&
|
|
||||||
other.enabled == enabled;
|
other.enabled == enabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(channel.hashCode) +
|
|
||||||
(enabled.hashCode);
|
(enabled.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigNewVersionCheckDto[channel=$channel, enabled=$enabled]';
|
String toString() => 'SystemConfigNewVersionCheckDto[enabled=$enabled]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'channel'] = this.channel;
|
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -52,7 +46,6 @@ class SystemConfigNewVersionCheckDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return SystemConfigNewVersionCheckDto(
|
return SystemConfigNewVersionCheckDto(
|
||||||
channel: ReleaseChannel.fromJson(json[r'channel'])!,
|
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,7 +94,6 @@ class SystemConfigNewVersionCheckDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'channel',
|
|
||||||
'enabled',
|
'enabled',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ void main() {
|
|||||||
when(() => mockApi.serverInfoApi).thenReturn(mockServerApi);
|
when(() => mockApi.serverInfoApi).thenReturn(mockServerApi);
|
||||||
when(
|
when(
|
||||||
() => mockServerApi.getServerVersion(),
|
() => mockServerApi.getServerVersion(),
|
||||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0, prerelease: null));
|
).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0));
|
||||||
|
|
||||||
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
|
when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler);
|
||||||
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
|
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler);
|
||||||
@@ -559,7 +559,7 @@ void main() {
|
|||||||
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
||||||
when(
|
when(
|
||||||
() => mockServerApi.getServerVersion(),
|
() => mockServerApi.getServerVersion(),
|
||||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null));
|
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1));
|
||||||
|
|
||||||
await sut.sync();
|
await sut.sync();
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@ void main() {
|
|||||||
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
await Store.put(StoreKey.syncMigrationStatus, "[]");
|
||||||
when(
|
when(
|
||||||
() => mockServerApi.getServerVersion(),
|
() => mockServerApi.getServerVersion(),
|
||||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0, prerelease: null));
|
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0));
|
||||||
await sut.sync();
|
await sut.sync();
|
||||||
|
|
||||||
verifyInOrder([
|
verifyInOrder([
|
||||||
@@ -617,7 +617,7 @@ void main() {
|
|||||||
|
|
||||||
when(
|
when(
|
||||||
() => mockServerApi.getServerVersion(),
|
() => mockServerApi.getServerVersion(),
|
||||||
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1, prerelease: null));
|
).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1));
|
||||||
|
|
||||||
await sut.sync();
|
await sut.sync();
|
||||||
|
|
||||||
|
|||||||
@@ -88,71 +88,5 @@ void main() {
|
|||||||
expect(version2.minor, 2);
|
expect(version2.minor, 2);
|
||||||
expect(version2.patch, 3);
|
expect(version2.patch, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Orders later prerelease above earlier prerelease', () {
|
|
||||||
const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1);
|
|
||||||
const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2);
|
|
||||||
expect(rc2 > rc1, isTrue);
|
|
||||||
expect(rc1 < rc2, isTrue);
|
|
||||||
expect(rc1 == rc2, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Final release outranks its prerelease of the same version', () {
|
|
||||||
const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1);
|
|
||||||
const release = SemVer(major: 1, minor: 151, patch: 0);
|
|
||||||
expect(release > rc, isTrue);
|
|
||||||
expect(rc < release, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Higher major outranks a prerelease regardless of ordinal', () {
|
|
||||||
const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 9);
|
|
||||||
const next = SemVer(major: 2, minor: 0, patch: 0);
|
|
||||||
expect(next > rc, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Equal prerelease versions compare as equal', () {
|
|
||||||
const a = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3);
|
|
||||||
const b = SemVer(major: 1, minor: 151, patch: 0, prerelease: 3);
|
|
||||||
expect(a == b, isTrue);
|
|
||||||
expect(a > b, isFalse);
|
|
||||||
expect(a < b, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Reports prerelease difference type', () {
|
|
||||||
const rc1 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 1);
|
|
||||||
const rc2 = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2);
|
|
||||||
expect(rc1.differenceType(rc2), SemVerType.prerelease);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('toString includes prerelease suffix when present', () {
|
|
||||||
const rc = SemVer(major: 1, minor: 151, patch: 0, prerelease: 2);
|
|
||||||
expect(rc.toString(), '1.151.0-rc.2');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Parses prerelease ordinal from -rc strings', () {
|
|
||||||
final dotted = SemVer.fromString('1.151.0-rc.2');
|
|
||||||
expect(dotted.major, 1);
|
|
||||||
expect(dotted.minor, 151);
|
|
||||||
expect(dotted.patch, 0);
|
|
||||||
expect(dotted.prerelease, 2);
|
|
||||||
|
|
||||||
expect(SemVer.fromString('v1.151.0-rc.3').prerelease, 3);
|
|
||||||
expect(SemVer.fromString('1.2.3-rc.2+build.5').prerelease, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Plain version string has null prerelease', () {
|
|
||||||
expect(SemVer.fromString('3.0.0').prerelease, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Invalid rc suffixes parse without error and have null prerelease', () {
|
|
||||||
final debug = SemVer.fromString('1.2.3-debug');
|
|
||||||
expect(debug.major, 1);
|
|
||||||
expect(debug.minor, 2);
|
|
||||||
expect(debug.patch, 3);
|
|
||||||
expect(debug.prerelease, isNull);
|
|
||||||
|
|
||||||
expect(SemVer.fromString('1.2.3+build.5').prerelease, isNull);
|
|
||||||
expect(SemVer.fromString('1.151.0-rc4').prerelease, isNull);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20800,58 +20800,6 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"ReleaseChannel": {
|
|
||||||
"description": "Release channel",
|
|
||||||
"enum": [
|
|
||||||
"stable",
|
|
||||||
"releaseCandidate"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ReleaseEventV1": {
|
|
||||||
"properties": {
|
|
||||||
"checkedAt": {
|
|
||||||
"description": "When the server last checked for a latest version. As an ISO timestamp",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"isAvailable": {
|
|
||||||
"description": "Whether a new version is available",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"releaseVersion": {
|
|
||||||
"$ref": "#/components/schemas/ServerVersionResponseDto"
|
|
||||||
},
|
|
||||||
"serverVersion": {
|
|
||||||
"$ref": "#/components/schemas/ServerVersionResponseDto"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"$ref": "#/components/schemas/ReleaseType",
|
|
||||||
"description": "Release type",
|
|
||||||
"nullable": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"checkedAt",
|
|
||||||
"isAvailable",
|
|
||||||
"releaseVersion",
|
|
||||||
"serverVersion",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"ReleaseType": {
|
|
||||||
"enum": [
|
|
||||||
"major",
|
|
||||||
"premajor",
|
|
||||||
"minor",
|
|
||||||
"preminor",
|
|
||||||
"patch",
|
|
||||||
"prepatch",
|
|
||||||
"prerelease",
|
|
||||||
"release"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ReverseGeocodingStateResponseDto": {
|
"ReverseGeocodingStateResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"lastImportFileName": {
|
"lastImportFileName": {
|
||||||
@@ -21521,40 +21469,26 @@
|
|||||||
"major": {
|
"major": {
|
||||||
"description": "Major version number",
|
"description": "Major version number",
|
||||||
"maximum": 9007199254740991,
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": -9007199254740991,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"minor": {
|
"minor": {
|
||||||
"description": "Minor version number",
|
"description": "Minor version number",
|
||||||
"maximum": 9007199254740991,
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": -9007199254740991,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"patch": {
|
"patch": {
|
||||||
"description": "Patch version number",
|
"description": "Patch version number",
|
||||||
"maximum": 9007199254740991,
|
"maximum": 9007199254740991,
|
||||||
"minimum": 0,
|
"minimum": -9007199254740991,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
|
||||||
"prerelease": {
|
|
||||||
"description": "Pre-release version number",
|
|
||||||
"maximum": 9007199254740991,
|
|
||||||
"minimum": 0,
|
|
||||||
"nullable": true,
|
|
||||||
"type": "integer",
|
|
||||||
"x-immich-history": [
|
|
||||||
{
|
|
||||||
"version": "v3.0.0",
|
|
||||||
"state": "Added"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"major",
|
"major",
|
||||||
"minor",
|
"minor",
|
||||||
"patch",
|
"patch"
|
||||||
"prerelease"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@@ -24575,16 +24509,12 @@
|
|||||||
},
|
},
|
||||||
"SystemConfigNewVersionCheckDto": {
|
"SystemConfigNewVersionCheckDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"channel": {
|
|
||||||
"$ref": "#/components/schemas/ReleaseChannel"
|
|
||||||
},
|
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"description": "Enabled",
|
"description": "Enabled",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"channel",
|
|
||||||
"enabled"
|
"enabled"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|||||||
@@ -2074,8 +2074,6 @@ export type ServerVersionResponseDto = {
|
|||||||
minor: number;
|
minor: number;
|
||||||
/** Patch version number */
|
/** Patch version number */
|
||||||
patch: number;
|
patch: number;
|
||||||
/** Pre-release version number */
|
|
||||||
prerelease: number | null;
|
|
||||||
};
|
};
|
||||||
export type VersionCheckStateResponseDto = {
|
export type VersionCheckStateResponseDto = {
|
||||||
/** Last check timestamp */
|
/** Last check timestamp */
|
||||||
@@ -2423,7 +2421,6 @@ export type SystemConfigMetadataDto = {
|
|||||||
faces: SystemConfigFacesDto;
|
faces: SystemConfigFacesDto;
|
||||||
};
|
};
|
||||||
export type SystemConfigNewVersionCheckDto = {
|
export type SystemConfigNewVersionCheckDto = {
|
||||||
channel: ReleaseChannel;
|
|
||||||
/** Enabled */
|
/** Enabled */
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
@@ -2769,16 +2766,6 @@ export type WorkflowShareResponseDto = {
|
|||||||
trigger: WorkflowTrigger;
|
trigger: WorkflowTrigger;
|
||||||
};
|
};
|
||||||
export type LicenseResponseDto = UserLicense;
|
export type LicenseResponseDto = UserLicense;
|
||||||
export type ReleaseEventV1 = {
|
|
||||||
/** When the server last checked for a latest version. As an ISO timestamp */
|
|
||||||
checkedAt: string;
|
|
||||||
/** Whether a new version is available */
|
|
||||||
isAvailable: boolean;
|
|
||||||
releaseVersion: ServerVersionResponseDto;
|
|
||||||
serverVersion: ServerVersionResponseDto;
|
|
||||||
/** Release type */
|
|
||||||
"type": ReleaseType;
|
|
||||||
};
|
|
||||||
export type SyncAckV1 = {};
|
export type SyncAckV1 = {};
|
||||||
export type SyncAlbumDeleteV1 = {
|
export type SyncAlbumDeleteV1 = {
|
||||||
/** Album ID */
|
/** Album ID */
|
||||||
@@ -7318,10 +7305,6 @@ export enum LogLevel {
|
|||||||
Error = "error",
|
Error = "error",
|
||||||
Fatal = "fatal"
|
Fatal = "fatal"
|
||||||
}
|
}
|
||||||
export enum ReleaseChannel {
|
|
||||||
Stable = "stable",
|
|
||||||
ReleaseCandidate = "releaseCandidate"
|
|
||||||
}
|
|
||||||
export enum OAuthTokenEndpointAuthMethod {
|
export enum OAuthTokenEndpointAuthMethod {
|
||||||
ClientSecretPost = "client_secret_post",
|
ClientSecretPost = "client_secret_post",
|
||||||
ClientSecretBasic = "client_secret_basic"
|
ClientSecretBasic = "client_secret_basic"
|
||||||
@@ -7330,16 +7313,6 @@ export enum AssetOrderBy {
|
|||||||
TakenAt = "takenAt",
|
TakenAt = "takenAt",
|
||||||
CreatedAt = "createdAt"
|
CreatedAt = "createdAt"
|
||||||
}
|
}
|
||||||
export enum ReleaseType {
|
|
||||||
Major = "major",
|
|
||||||
Premajor = "premajor",
|
|
||||||
Minor = "minor",
|
|
||||||
Preminor = "preminor",
|
|
||||||
Patch = "patch",
|
|
||||||
Prepatch = "prepatch",
|
|
||||||
Prerelease = "prerelease",
|
|
||||||
Release = "release"
|
|
||||||
}
|
|
||||||
export enum UserMetadataKey {
|
export enum UserMetadataKey {
|
||||||
Preferences = "preferences",
|
Preferences = "preferences",
|
||||||
License = "license",
|
License = "license",
|
||||||
|
|||||||
Generated
+20
-27
@@ -571,8 +571,8 @@ importers:
|
|||||||
specifier: ^1.6.3
|
specifier: ^1.6.3
|
||||||
version: 1.6.4
|
version: 1.6.4
|
||||||
semver:
|
semver:
|
||||||
specifier: ^7.8.1
|
specifier: ^7.6.2
|
||||||
version: 7.8.1
|
version: 7.8.0
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.34.5
|
specifier: ^0.34.5
|
||||||
version: 0.34.5
|
version: 0.34.5
|
||||||
@@ -11243,11 +11243,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
semver@7.8.1:
|
|
||||||
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
send@0.19.2:
|
send@0.19.2:
|
||||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -16305,7 +16300,7 @@ snapshots:
|
|||||||
nopt: 5.0.0
|
nopt: 5.0.0
|
||||||
npmlog: 5.0.1
|
npmlog: 5.0.1
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
@@ -17762,7 +17757,7 @@ snapshots:
|
|||||||
'@testing-library/dom@10.4.1':
|
'@testing-library/dom@10.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.0
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.2
|
||||||
'@types/aria-query': 5.0.4
|
'@types/aria-query': 5.0.4
|
||||||
aria-query: 5.3.0
|
aria-query: 5.3.0
|
||||||
dom-accessibility-api: 0.5.16
|
dom-accessibility-api: 0.5.16
|
||||||
@@ -18466,7 +18461,7 @@ snapshots:
|
|||||||
'@typescript-eslint/visitor-keys': 8.59.4
|
'@typescript-eslint/visitor-keys': 8.59.4
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.16
|
||||||
ts-api-utils: 2.5.0(typescript@6.0.3)
|
ts-api-utils: 2.5.0(typescript@6.0.3)
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
@@ -19566,7 +19561,7 @@ snapshots:
|
|||||||
dot-prop: 10.1.0
|
dot-prop: 10.1.0
|
||||||
env-paths: 3.0.0
|
env-paths: 3.0.0
|
||||||
json-schema-typed: 8.0.2
|
json-schema-typed: 8.0.2
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
uint8array-extras: 1.5.0
|
uint8array-extras: 1.5.0
|
||||||
|
|
||||||
config-chain@1.1.13:
|
config-chain@1.1.13:
|
||||||
@@ -19738,7 +19733,7 @@ snapshots:
|
|||||||
postcss-modules-scope: 3.2.1(postcss@8.5.15)
|
postcss-modules-scope: 3.2.1(postcss@8.5.15)
|
||||||
postcss-modules-values: 4.0.0(postcss@8.5.15)
|
postcss-modules-values: 4.0.0(postcss@8.5.15)
|
||||||
postcss-value-parser: 4.2.0
|
postcss-value-parser: 4.2.0
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
webpack: 5.107.0(postcss@8.5.15)
|
webpack: 5.107.0(postcss@8.5.15)
|
||||||
|
|
||||||
@@ -20606,7 +20601,7 @@ snapshots:
|
|||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
globals: 15.15.0
|
globals: 15.15.0
|
||||||
lodash.memoize: 4.1.2
|
lodash.memoize: 4.1.2
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
|
|
||||||
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3):
|
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -20629,7 +20624,7 @@ snapshots:
|
|||||||
postcss: 8.5.15
|
postcss: 8.5.15
|
||||||
postcss-load-config: 3.1.4(postcss@8.5.15)
|
postcss-load-config: 3.1.4(postcss@8.5.15)
|
||||||
postcss-safe-parser: 7.0.1(postcss@8.5.15)
|
postcss-safe-parser: 7.0.1(postcss@8.5.15)
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
svelte-eslint-parser: 1.6.1(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
svelte-eslint-parser: 1.6.1(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
||||||
@@ -21107,7 +21102,7 @@ snapshots:
|
|||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
node-abort-controller: 3.1.1
|
node-abort-controller: 3.1.1
|
||||||
schema-utils: 3.3.0
|
schema-utils: 3.3.0
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
tapable: 2.3.3
|
tapable: 2.3.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)
|
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||||
@@ -21543,7 +21538,7 @@ snapshots:
|
|||||||
|
|
||||||
history@4.10.1:
|
history@4.10.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.2
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
resolve-pathname: 3.0.0
|
resolve-pathname: 3.0.0
|
||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
@@ -22131,7 +22126,7 @@ snapshots:
|
|||||||
lodash.isstring: 4.0.1
|
lodash.isstring: 4.0.1
|
||||||
lodash.once: 4.1.1
|
lodash.once: 4.1.1
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
|
|
||||||
just-compare@2.3.0: {}
|
just-compare@2.3.0: {}
|
||||||
|
|
||||||
@@ -22417,7 +22412,7 @@ snapshots:
|
|||||||
|
|
||||||
make-dir@4.0.0:
|
make-dir@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
|
|
||||||
maplibre-gl@5.24.0:
|
maplibre-gl@5.24.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -23252,7 +23247,7 @@ snapshots:
|
|||||||
|
|
||||||
node-abi@3.92.0:
|
node-abi@3.92.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
node-abort-controller@3.1.1: {}
|
node-abort-controller@3.1.1: {}
|
||||||
@@ -23293,7 +23288,7 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
nopt: 9.0.0
|
nopt: 9.0.0
|
||||||
proc-log: 6.1.0
|
proc-log: 6.1.0
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
tar: 7.5.15
|
tar: 7.5.15
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.16
|
||||||
undici: 6.25.0
|
undici: 6.25.0
|
||||||
@@ -23531,7 +23526,7 @@ snapshots:
|
|||||||
got: 12.6.1
|
got: 12.6.1
|
||||||
registry-auth-token: 5.1.1
|
registry-auth-token: 5.1.1
|
||||||
registry-url: 6.0.1
|
registry-url: 6.0.1
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
|
|
||||||
package-manager-detector@1.6.0: {}
|
package-manager-detector@1.6.0: {}
|
||||||
|
|
||||||
@@ -23919,7 +23914,7 @@ snapshots:
|
|||||||
cosmiconfig: 8.3.6(typescript@6.0.3)
|
cosmiconfig: 8.3.6(typescript@6.0.3)
|
||||||
jiti: 1.21.7
|
jiti: 1.21.7
|
||||||
postcss: 8.5.15
|
postcss: 8.5.15
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
webpack: 5.107.0(postcss@8.5.15)
|
webpack: 5.107.0(postcss@8.5.15)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
@@ -24974,14 +24969,12 @@ snapshots:
|
|||||||
|
|
||||||
semver-diff@4.0.0:
|
semver-diff@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.8.0: {}
|
semver@7.8.0: {}
|
||||||
|
|
||||||
semver@7.8.1: {}
|
|
||||||
|
|
||||||
send@0.19.2:
|
send@0.19.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
@@ -25516,7 +25509,7 @@ snapshots:
|
|||||||
postcss: 8.5.15
|
postcss: 8.5.15
|
||||||
postcss-scss: 4.0.9(postcss@8.5.15)
|
postcss-scss: 4.0.9(postcss@8.5.15)
|
||||||
postcss-selector-parser: 7.1.1
|
postcss-selector-parser: 7.1.1
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
||||||
|
|
||||||
@@ -26224,7 +26217,7 @@ snapshots:
|
|||||||
is-yarn-global: 0.4.1
|
is-yarn-global: 0.4.1
|
||||||
latest-version: 7.0.0
|
latest-version: 7.0.0
|
||||||
pupa: 3.3.0
|
pupa: 3.3.0
|
||||||
semver: 7.8.1
|
semver: 7.8.0
|
||||||
semver-diff: 4.0.0
|
semver-diff: 4.0.0
|
||||||
xdg-basedir: 5.1.0
|
xdg-basedir: 5.1.0
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@
|
|||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"semver": "^7.8.1",
|
"semver": "^7.6.2",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"sirv": "^3.0.0",
|
"sirv": "^3.0.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CronExpression } from '@nestjs/schedule';
|
import { CronExpression } from '@nestjs/schedule';
|
||||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
|
||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
@@ -136,7 +135,6 @@ export type SystemConfig = {
|
|||||||
};
|
};
|
||||||
newVersionCheck: {
|
newVersionCheck: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
channel: ReleaseChannel;
|
|
||||||
};
|
};
|
||||||
nightlyTasks: {
|
nightlyTasks: {
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@@ -346,7 +344,6 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
},
|
},
|
||||||
newVersionCheck: {
|
newVersionCheck: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
channel: ReleaseChannel.Stable,
|
|
||||||
},
|
},
|
||||||
nightlyTasks: {
|
nightlyTasks: {
|
||||||
startTime: '00:00',
|
startTime: '00:00',
|
||||||
|
|||||||
@@ -265,13 +265,3 @@ export class HistoryBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
||||||
export const extraModels: Function[] = [];
|
|
||||||
|
|
||||||
export const ExtraModel = (): ClassDecorator => {
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-unsafe-function-type
|
|
||||||
return (object: Function) => {
|
|
||||||
extraModels.push(object);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createZodDto } from 'nestjs-zod';
|
import { createZodDto } from 'nestjs-zod';
|
||||||
import type { SemVer } from 'semver';
|
import type { SemVer } from 'semver';
|
||||||
import { ExtraModel, HistoryBuilder } from 'src/decorators';
|
|
||||||
import { isoDatetimeToDate } from 'src/validation';
|
import { isoDatetimeToDate } from 'src/validation';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
@@ -59,15 +58,9 @@ const ServerStorageResponseSchema = z
|
|||||||
|
|
||||||
const ServerVersionResponseSchema = z
|
const ServerVersionResponseSchema = z
|
||||||
.object({
|
.object({
|
||||||
major: z.int().min(0).describe('Major version number'),
|
major: z.int().describe('Major version number'),
|
||||||
minor: z.int().min(0).describe('Minor version number'),
|
minor: z.int().describe('Minor version number'),
|
||||||
patch: z.int().min(0).describe('Patch version number'),
|
patch: z.int().describe('Patch version number'),
|
||||||
prerelease: z
|
|
||||||
.int()
|
|
||||||
.min(0)
|
|
||||||
.nullable()
|
|
||||||
.meta(HistoryBuilder.v3().getExtensions())
|
|
||||||
.describe('Pre-release version number'),
|
|
||||||
})
|
})
|
||||||
.meta({ id: 'ServerVersionResponseDto' });
|
.meta({ id: 'ServerVersionResponseDto' });
|
||||||
|
|
||||||
@@ -147,27 +140,6 @@ const ServerFeaturesSchema = z
|
|||||||
})
|
})
|
||||||
.meta({ id: 'ServerFeaturesDto' });
|
.meta({ id: 'ServerFeaturesDto' });
|
||||||
|
|
||||||
export enum ReleaseType {
|
|
||||||
Major = 'major',
|
|
||||||
Premajor = 'premajor',
|
|
||||||
Minor = 'minor',
|
|
||||||
Preminor = 'preminor',
|
|
||||||
Patch = 'patch',
|
|
||||||
Prepatch = 'prepatch',
|
|
||||||
Prerelease = 'prerelease',
|
|
||||||
Release = 'release',
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReleaseTypeSchema = z.enum(ReleaseType).meta({ id: 'ReleaseType' }).describe('Release type');
|
|
||||||
|
|
||||||
const ReleaseEventV1Schema = z.object({
|
|
||||||
isAvailable: z.boolean().describe('Whether a new version is available'),
|
|
||||||
checkedAt: z.string().describe('When the server last checked for a latest version. As an ISO timestamp'),
|
|
||||||
serverVersion: ServerVersionResponseSchema,
|
|
||||||
releaseVersion: ServerVersionResponseSchema,
|
|
||||||
type: ReleaseTypeSchema.nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {}
|
export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {}
|
||||||
export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {}
|
export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {}
|
||||||
export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {}
|
export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {}
|
||||||
@@ -175,12 +147,7 @@ export class ServerStorageResponseDto extends createZodDto(ServerStorageResponse
|
|||||||
|
|
||||||
export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) {
|
export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) {
|
||||||
static fromSemVer(value: SemVer): z.infer<typeof ServerVersionResponseSchema> {
|
static fromSemVer(value: SemVer): z.infer<typeof ServerVersionResponseSchema> {
|
||||||
return {
|
return { major: value.major, minor: value.minor, patch: value.patch };
|
||||||
major: value.major,
|
|
||||||
minor: value.minor,
|
|
||||||
patch: value.patch,
|
|
||||||
prerelease: (value.prerelease[1] as number) ?? null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,5 +158,10 @@ export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesRe
|
|||||||
export class ServerConfigDto extends createZodDto(ServerConfigSchema) {}
|
export class ServerConfigDto extends createZodDto(ServerConfigSchema) {}
|
||||||
export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {}
|
export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {}
|
||||||
|
|
||||||
@ExtraModel()
|
export interface ReleaseNotification {
|
||||||
export class ReleaseEventV1 extends createZodDto(ReleaseEventV1Schema) {}
|
isAvailable: boolean;
|
||||||
|
/** ISO8601 */
|
||||||
|
checkedAt: string;
|
||||||
|
serverVersion: ServerVersionResponseDto;
|
||||||
|
releaseVersion: ServerVersionResponseDto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||||
import { createZodDto } from 'nestjs-zod';
|
import { createZodDto } from 'nestjs-zod';
|
||||||
import { ExtraModel } from 'src/decorators';
|
|
||||||
import { AssetEditActionSchema } from 'src/dtos/editing.dto';
|
import { AssetEditActionSchema } from 'src/dtos/editing.dto';
|
||||||
import {
|
import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
@@ -17,6 +17,15 @@ import {
|
|||||||
import { isoDatetimeToDate } from 'src/validation';
|
import { isoDatetimeToDate } from 'src/validation';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const extraSyncModels: Function[] = [];
|
||||||
|
|
||||||
|
const ExtraModel = (): ClassDecorator => {
|
||||||
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||||
|
return (object: Function) => {
|
||||||
|
extraSyncModels.push(object);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const SyncUserV1Schema = z
|
const SyncUserV1Schema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().describe('User ID'),
|
id: z.string().describe('User ID'),
|
||||||
|
|||||||
@@ -151,15 +151,8 @@ const SystemConfigMapSchema = z
|
|||||||
})
|
})
|
||||||
.meta({ id: 'SystemConfigMapDto' });
|
.meta({ id: 'SystemConfigMapDto' });
|
||||||
|
|
||||||
export enum ReleaseChannel {
|
|
||||||
Stable = 'stable',
|
|
||||||
ReleaseCandidate = 'releaseCandidate',
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' });
|
|
||||||
|
|
||||||
const SystemConfigNewVersionCheckSchema = z
|
const SystemConfigNewVersionCheckSchema = z
|
||||||
.object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema })
|
.object({ enabled: configBool.describe('Enabled') })
|
||||||
.meta({ id: 'SystemConfigNewVersionCheckDto' });
|
.meta({ id: 'SystemConfigNewVersionCheckDto' });
|
||||||
|
|
||||||
const SystemConfigNightlyTasksSchema = z
|
const SystemConfigNightlyTasksSchema = z
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { exec as execCallback } from 'node:child_process';
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
|
||||||
@@ -65,12 +64,10 @@ export class ServerInfoRepository {
|
|||||||
this.logger.setContext(ServerInfoRepository.name);
|
this.logger.setContext(ServerInfoRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLatestRelease(channel: ReleaseChannel): Promise<VersionResponse> {
|
async getLatestRelease(): Promise<VersionResponse> {
|
||||||
try {
|
try {
|
||||||
const { versionCheck } = this.configRepository.getEnv();
|
const { versionCheck } = this.configRepository.getEnv();
|
||||||
const url = new URL(versionCheck.url);
|
const response = await fetch(versionCheck.url);
|
||||||
url.searchParams.append('channel', channel);
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Version check request failed with status ${response.status}: ${await response.text()}`);
|
throw new Error(`Version check request failed with status ${response.status}: ${await response.text()}`);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Server, Socket } from 'socket.io';
|
|||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||||
import { ReleaseEventV1, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto';
|
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto';
|
||||||
import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository';
|
import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
@@ -31,7 +31,7 @@ export interface ClientEventMap {
|
|||||||
on_person_thumbnail: [string];
|
on_person_thumbnail: [string];
|
||||||
on_server_version: [ServerVersionResponseDto];
|
on_server_version: [ServerVersionResponseDto];
|
||||||
on_config_update: [];
|
on_config_update: [];
|
||||||
on_new_release: [ReleaseEventV1];
|
on_new_release: [ReleaseNotification];
|
||||||
on_notification: [NotificationDto];
|
on_notification: [NotificationDto];
|
||||||
on_session_delete: [string];
|
on_session_delete: [string];
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class SearchService extends BaseService {
|
|||||||
|
|
||||||
const page = dto.page ?? 1;
|
const page = dto.page ?? 1;
|
||||||
const size = dto.size || 250;
|
const size = dto.size || 250;
|
||||||
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
|
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
|
||||||
{ page, size },
|
{ page, size },
|
||||||
{
|
{
|
||||||
@@ -103,7 +103,7 @@ export class SearchService extends BaseService {
|
|||||||
requireElevatedPermission(auth);
|
requireElevatedPermission(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
|
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
|
||||||
return items.map((item) => mapAsset(item, { auth }));
|
return items.map((item) => mapAsset(item, { auth }));
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ export class SearchService extends BaseService {
|
|||||||
requireElevatedPermission(auth);
|
requireElevatedPermission(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { ...dto, userIds });
|
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { ...dto, userIds });
|
||||||
return items.map((item) => mapAsset(item, { auth }));
|
return items.map((item) => mapAsset(item, { auth }));
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ export class SearchService extends BaseService {
|
|||||||
throw new BadRequestException('Smart search is not enabled');
|
throw new BadRequestException('Smart search is not enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIds = this.getUserIdsToSearch(auth, dto.visibility);
|
const userIds = this.getUserIdsToSearch(auth);
|
||||||
let embedding;
|
let embedding;
|
||||||
if (dto.query) {
|
if (dto.query) {
|
||||||
const key = machineLearning.clip.modelName + dto.query + dto.language;
|
const key = machineLearning.clip.modelName + dto.query + dto.language;
|
||||||
@@ -202,11 +202,7 @@ export class SearchService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUserIdsToSearch(auth: AuthDto, visibility?: AssetVisibility): Promise<string[]> {
|
private async getUserIdsToSearch(auth: AuthDto): Promise<string[]> {
|
||||||
// Locked assets are personal. Never include partner IDs, regardless of A's elevated session.
|
|
||||||
if (visibility === AssetVisibility.Locked) {
|
|
||||||
return [auth.user.id];
|
|
||||||
}
|
|
||||||
const partnerIds = await getMyPartnerIds({
|
const partnerIds = await getMyPartnerIds({
|
||||||
userId: auth.user.id,
|
userId: auth.user.id,
|
||||||
repository: this.partnerRepository,
|
repository: this.partnerRepository,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { defaults, SystemConfig } from 'src/config';
|
import { defaults, SystemConfig } from 'src/config';
|
||||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
|
||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
@@ -185,7 +184,6 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
},
|
},
|
||||||
newVersionCheck: {
|
newVersionCheck: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
channel: ReleaseChannel.Stable,
|
|
||||||
},
|
},
|
||||||
trash: {
|
trash: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -204,16 +204,5 @@ describe(TimelineService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).rejects.toThrow(BadRequestException);
|
).rejects.toThrow(BadRequestException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if withPartners is true and visibility is locked', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.adminWithElevatedPermission, {
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
visibility: AssetVisibility.Locked,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.adminWithElevatedPermission.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrow(BadRequestException);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,14 +71,13 @@ export class TimelineService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dto.withPartners) {
|
if (dto.withPartners) {
|
||||||
const requestedLocked = dto.visibility === AssetVisibility.Locked;
|
|
||||||
const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
||||||
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||||
const requestedTrash = dto.isTrashed === true;
|
const requestedTrash = dto.isTrashed === true;
|
||||||
|
|
||||||
if (requestedLocked || requestedArchived || requestedFavorite || requestedTrash) {
|
if (requestedArchived || requestedFavorite || requestedTrash) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { DateTime } from 'luxon';
|
|||||||
import { SemVer } from 'semver';
|
import { SemVer } from 'semver';
|
||||||
import { defaults } from 'src/config';
|
import { defaults } from 'src/config';
|
||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
|
||||||
import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
||||||
import { VersionService } from 'src/services/version.service';
|
import { VersionService } from 'src/services/version.service';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
@@ -23,17 +22,6 @@ describe(VersionService.name, () => {
|
|||||||
mocks.cron.update.mockResolvedValue();
|
mocks.cron.update.mockResolvedValue();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
vitest.mock(import('src/constants.js'), async () => ({
|
|
||||||
...(await vitest.importActual<typeof import('src/constants.js')>('src/constants.js')),
|
|
||||||
serverVersion: new SemVer('v3.0.0'),
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
vitest.unmock(import('src/constants.js'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -78,10 +66,9 @@ describe(VersionService.name, () => {
|
|||||||
describe('getVersion', () => {
|
describe('getVersion', () => {
|
||||||
it('should respond the server version', () => {
|
it('should respond the server version', () => {
|
||||||
expect(sut.getVersion()).toEqual({
|
expect(sut.getVersion()).toEqual({
|
||||||
major: 3,
|
major: serverVersion.major,
|
||||||
minor: 0,
|
minor: serverVersion.minor,
|
||||||
patch: 0,
|
patch: serverVersion.patch,
|
||||||
prerelease: null,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -156,24 +143,24 @@ describe(VersionService.name, () => {
|
|||||||
describe('onConfigUpdate', () => {
|
describe('onConfigUpdate', () => {
|
||||||
it('should queue a version check job when newVersionCheck is enabled', async () => {
|
it('should queue a version check job when newVersionCheck is enabled', async () => {
|
||||||
await sut.onConfigUpdate({
|
await sut.onConfigUpdate({
|
||||||
oldConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } },
|
oldConfig: { ...defaults, newVersionCheck: { enabled: false } },
|
||||||
newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||||
});
|
});
|
||||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not queue a version check job when newVersionCheck is disabled', async () => {
|
it('should not queue a version check job when newVersionCheck is disabled', async () => {
|
||||||
await sut.onConfigUpdate({
|
await sut.onConfigUpdate({
|
||||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||||
newConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } },
|
newConfig: { ...defaults, newVersionCheck: { enabled: false } },
|
||||||
});
|
});
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not queue a version check job when newVersionCheck was already enabled', async () => {
|
it('should not queue a version check job when newVersionCheck was already enabled', async () => {
|
||||||
await sut.onConfigUpdate({
|
await sut.onConfigUpdate({
|
||||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||||
newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||||
});
|
});
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -182,36 +169,21 @@ describe(VersionService.name, () => {
|
|||||||
describe('onWebsocketConnection', () => {
|
describe('onWebsocketConnection', () => {
|
||||||
it('should send on_server_version client event', async () => {
|
it('should send on_server_version client event', async () => {
|
||||||
await sut.onWebsocketConnection({ userId: '42' });
|
await sut.onWebsocketConnection({ userId: '42' });
|
||||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||||
major: 3,
|
|
||||||
minor: 0,
|
|
||||||
patch: 0,
|
|
||||||
prerelease: null,
|
|
||||||
});
|
|
||||||
expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(1);
|
expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should also send a new release notification', async () => {
|
it('should also send a new release notification', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' });
|
mocks.systemMetadata.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' });
|
||||||
await sut.onWebsocketConnection({ userId: '42' });
|
await sut.onWebsocketConnection({ userId: '42' });
|
||||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||||
major: 3,
|
|
||||||
minor: 0,
|
|
||||||
patch: 0,
|
|
||||||
prerelease: null,
|
|
||||||
});
|
|
||||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not send a release notification when the version check is disabled', async () => {
|
it('should not send a release notification when the version check is disabled', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } });
|
mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } });
|
||||||
await sut.onWebsocketConnection({ userId: '42' });
|
await sut.onWebsocketConnection({ userId: '42' });
|
||||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||||
major: 3,
|
|
||||||
minor: 0,
|
|
||||||
patch: 0,
|
|
||||||
prerelease: null,
|
|
||||||
});
|
|
||||||
expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,27 +3,19 @@ import { DateTime } from 'luxon';
|
|||||||
import semver, { SemVer } from 'semver';
|
import semver, { SemVer } from 'semver';
|
||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
import { ReleaseEventV1, ReleaseType, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
|
||||||
import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
||||||
import { ArgOf } from 'src/repositories/event.repository';
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { VersionCheckMetadata } from 'src/types';
|
import { VersionCheckMetadata } from 'src/types';
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
|
|
||||||
const asNotification = (
|
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
||||||
channel: ReleaseChannel,
|
|
||||||
{ checkedAt, releaseVersion }: VersionCheckMetadata,
|
|
||||||
): ReleaseEventV1 => {
|
|
||||||
return {
|
return {
|
||||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
isAvailable: semver.gt(releaseVersion, serverVersion),
|
||||||
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
|
||||||
includePrerelease: channel === ReleaseChannel.ReleaseCandidate,
|
|
||||||
}),
|
|
||||||
checkedAt,
|
checkedAt,
|
||||||
serverVersion: ServerVersionResponseDto.fromSemVer(serverVersion),
|
serverVersion: ServerVersionResponseDto.fromSemVer(serverVersion),
|
||||||
releaseVersion: ServerVersionResponseDto.fromSemVer(new SemVer(releaseVersion)),
|
releaseVersion: ServerVersionResponseDto.fromSemVer(new SemVer(releaseVersion)),
|
||||||
type: semver.diff(serverVersion, releaseVersion) as ReleaseType,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,21 +98,14 @@ export class VersionService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease(
|
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease();
|
||||||
newVersionCheck.channel,
|
|
||||||
);
|
|
||||||
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
|
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
|
||||||
|
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.VersionCheckState, metadata);
|
await this.systemMetadataRepository.set(SystemMetadataKey.VersionCheckState, metadata);
|
||||||
|
|
||||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
if (semver.gt(releaseVersion, serverVersion)) {
|
||||||
if (
|
|
||||||
semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
|
||||||
includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`);
|
this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`);
|
||||||
this.websocketRepository.clientBroadcast('on_new_release', asNotification(newVersionCheck.channel, metadata));
|
this.websocketRepository.clientBroadcast('on_new_release', asNotification(metadata));
|
||||||
}
|
}
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`);
|
this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`);
|
||||||
@@ -132,11 +117,7 @@ export class VersionService extends BaseService {
|
|||||||
|
|
||||||
@OnEvent({ name: 'WebsocketConnect' })
|
@OnEvent({ name: 'WebsocketConnect' })
|
||||||
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
|
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
|
||||||
this.websocketRepository.clientSend(
|
this.websocketRepository.clientSend('on_server_version', userId, serverVersion);
|
||||||
'on_server_version',
|
|
||||||
userId,
|
|
||||||
ServerVersionResponseDto.fromSemVer(serverVersion),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { newVersionCheck } = await this.getConfig({ withCache: true });
|
const { newVersionCheck } = await this.getConfig({ withCache: true });
|
||||||
if (!newVersionCheck.enabled) {
|
if (!newVersionCheck.enabled) {
|
||||||
@@ -145,7 +126,7 @@ export class VersionService extends BaseService {
|
|||||||
|
|
||||||
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
this.websocketRepository.clientSend('on_new_release', userId, asNotification(newVersionCheck.channel, metadata));
|
this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import picomatch from 'picomatch';
|
|||||||
import parse from 'picomatch/lib/parse';
|
import parse from 'picomatch/lib/parse';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants';
|
import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants';
|
||||||
import { extraModels } from 'src/decorators';
|
import { extraSyncModels } from 'src/dtos/sync.dto';
|
||||||
import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
|
import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean })
|
|||||||
|
|
||||||
const options: SwaggerDocumentOptions = {
|
const options: SwaggerDocumentOptions = {
|
||||||
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
|
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
|
||||||
extraModels,
|
extraModels: extraSyncModels,
|
||||||
ignoreGlobalPrefix: true,
|
ignoreGlobalPrefix: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
-7
@@ -41,13 +41,6 @@ export const authStub = {
|
|||||||
id: 'token-id',
|
id: 'token-id',
|
||||||
} as AuthSession,
|
} as AuthSession,
|
||||||
}),
|
}),
|
||||||
adminWithElevatedPermission: Object.freeze<AuthDto>({
|
|
||||||
user: authUser.admin,
|
|
||||||
session: {
|
|
||||||
id: 'token-id-elevated',
|
|
||||||
hasElevatedPermission: true,
|
|
||||||
} as AuthSession,
|
|
||||||
}),
|
|
||||||
adminSharedLink: Object.freeze({
|
adminSharedLink: Object.freeze({
|
||||||
user: authUser.admin,
|
user: authUser.admin,
|
||||||
sharedLink: {
|
sharedLink: {
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ describe(TimelineService.name, () => {
|
|||||||
const response1 = sut.getTimeBuckets(auth, { withPartners: true, visibility: AssetVisibility.Archive });
|
const response1 = sut.getTimeBuckets(auth, { withPartners: true, visibility: AssetVisibility.Archive });
|
||||||
await expect(response1).rejects.toBeInstanceOf(BadRequestException);
|
await expect(response1).rejects.toBeInstanceOf(BadRequestException);
|
||||||
await expect(response1).rejects.toThrow(
|
await expect(response1).rejects.toThrow(
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
);
|
);
|
||||||
|
|
||||||
const response2 = sut.getTimeBuckets(auth, { withPartners: true });
|
const response2 = sut.getTimeBuckets(auth, { withPartners: true });
|
||||||
await expect(response2).rejects.toBeInstanceOf(BadRequestException);
|
await expect(response2).rejects.toBeInstanceOf(BadRequestException);
|
||||||
await expect(response2).rejects.toThrow(
|
await expect(response2).rejects.toThrow(
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,13 +67,13 @@ describe(TimelineService.name, () => {
|
|||||||
const response1 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: false });
|
const response1 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: false });
|
||||||
await expect(response1).rejects.toBeInstanceOf(BadRequestException);
|
await expect(response1).rejects.toBeInstanceOf(BadRequestException);
|
||||||
await expect(response1).rejects.toThrow(
|
await expect(response1).rejects.toThrow(
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
);
|
);
|
||||||
|
|
||||||
const response2 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: true });
|
const response2 = sut.getTimeBuckets(auth, { withPartners: true, isFavorite: true });
|
||||||
await expect(response2).rejects.toBeInstanceOf(BadRequestException);
|
await expect(response2).rejects.toBeInstanceOf(BadRequestException);
|
||||||
await expect(response2).rejects.toThrow(
|
await expect(response2).rejects.toThrow(
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ describe(TimelineService.name, () => {
|
|||||||
const response = sut.getTimeBuckets(auth, { withPartners: true, isTrashed: true });
|
const response = sut.getTimeBuckets(auth, { withPartners: true, isTrashed: true });
|
||||||
await expect(response).rejects.toBeInstanceOf(BadRequestException);
|
await expect(response).rejects.toBeInstanceOf(BadRequestException);
|
||||||
await expect(response).rejects.toThrow(
|
await expect(response).rejects.toThrow(
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
import ServerAboutModal from '$lib/modals/ServerAboutModal.svelte';
|
import ServerAboutModal from '$lib/modals/ServerAboutModal.svelte';
|
||||||
import { userInteraction } from '$lib/stores/user.svelte';
|
import { userInteraction } from '$lib/stores/user.svelte';
|
||||||
import { websocketStore } from '$lib/stores/websocket';
|
import { websocketStore } from '$lib/stores/websocket';
|
||||||
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
import { semverToName } from '$lib/utils';
|
import { semverToName } from '$lib/utils';
|
||||||
import { requestServerInfo } from '$lib/utils/auth';
|
import { requestServerInfo } from '$lib/utils/auth';
|
||||||
import {
|
import {
|
||||||
getAboutInfo,
|
getAboutInfo,
|
||||||
getVersionHistory,
|
getVersionHistory,
|
||||||
type ReleaseEventV1,
|
|
||||||
type ServerAboutResponseDto,
|
type ServerAboutResponseDto,
|
||||||
type ServerVersionHistoryResponseDto,
|
type ServerVersionHistoryResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
@@ -35,9 +35,11 @@
|
|||||||
userInteraction.versions = versions;
|
userInteraction.versions = versions;
|
||||||
});
|
});
|
||||||
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
|
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
|
||||||
let version = $derived($serverVersion ? semverToName($serverVersion) : null);
|
let version = $derived(
|
||||||
|
$serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null,
|
||||||
|
);
|
||||||
|
|
||||||
const getReleaseInfo = (release?: ReleaseEventV1) => {
|
const getReleaseInfo = (release?: ReleaseEvent) => {
|
||||||
if (!release || !release?.isAvailable || !authManager.user.isAdmin) {
|
if (!release || !release?.isAvailable || !authManager.user.isAdmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,14 @@
|
|||||||
|
|
||||||
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
|
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
|
||||||
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||||
|
|
||||||
|
const firstInOrNearViewport = $derived(viewerAssets.findIndex((a) => a.isInOrNearViewport));
|
||||||
|
const lastInOrNearViewport = $derived(viewerAssets.findLastIndex((a) => a.isInOrNearViewport));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
||||||
{#each viewerAssets as viewerAsset (viewerAsset.id)}
|
{#each viewerAssets.slice(firstInOrNearViewport, lastInOrNearViewport + 1) as viewerAsset (viewerAsset.id)}
|
||||||
{@const position = viewerAsset.position!}
|
{@const position = viewerAsset.position!}
|
||||||
{@const asset = viewerAsset.asset!}
|
{@const asset = viewerAsset.asset!}
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
<AssetLayout
|
<AssetLayout
|
||||||
{manager}
|
{manager}
|
||||||
viewerAssets={timelineDay.activeViewerAssets}
|
viewerAssets={timelineDay.viewerAssets}
|
||||||
height={timelineDay.height}
|
height={timelineDay.height}
|
||||||
width={timelineDay.width}
|
width={timelineDay.width}
|
||||||
{customThumbnailLayout}
|
{customThumbnailLayout}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import type {
|
|||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
PersonResponseDto,
|
PersonResponseDto,
|
||||||
QueueResponseDto,
|
QueueResponseDto,
|
||||||
ReleaseEventV1,
|
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SystemConfigDto,
|
SystemConfigDto,
|
||||||
TagResponseDto,
|
TagResponseDto,
|
||||||
UserAdminResponseDto,
|
UserAdminResponseDto,
|
||||||
WorkflowResponseDto,
|
WorkflowResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
||||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export type Events = {
|
|||||||
WorkflowUpdate: [WorkflowResponseDto];
|
WorkflowUpdate: [WorkflowResponseDto];
|
||||||
WorkflowDelete: [WorkflowResponseDto];
|
WorkflowDelete: [WorkflowResponseDto];
|
||||||
|
|
||||||
ReleaseEvent: [ReleaseEventV1];
|
ReleaseEvent: [ReleaseEvent];
|
||||||
|
|
||||||
WebsocketConnect: [];
|
WebsocketConnect: [];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { ReleaseEventV1 } from '@immich/sdk';
|
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import { type ReleaseEvent } from '$lib/types';
|
||||||
|
|
||||||
class ReleaseManager {
|
class ReleaseManager {
|
||||||
value = $state<ReleaseEventV1 | undefined>();
|
value = $state<ReleaseEvent | undefined>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
eventManager.on({
|
eventManager.on({
|
||||||
|
|||||||
@@ -53,3 +53,25 @@ export function updateTimelineMonthViewportProximity(timelineManager: TimelineMa
|
|||||||
timelineManager.clearDeferredLayout(month);
|
timelineManager.clearDeferredLayout(month);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateViewerAssetViewportProximity(
|
||||||
|
timelineManager: TimelineManager,
|
||||||
|
positionTop: number,
|
||||||
|
positionHeight: number,
|
||||||
|
) {
|
||||||
|
const headerHeight = timelineManager.headerHeight;
|
||||||
|
return calculateViewportProximity(
|
||||||
|
positionTop,
|
||||||
|
positionTop + positionHeight,
|
||||||
|
timelineManager.visibleWindow.top - headerHeight,
|
||||||
|
timelineManager.visibleWindow.bottom + headerHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateViewerAssetIsInOrNearViewport(
|
||||||
|
timelineManager: TimelineManager,
|
||||||
|
positionTop: number,
|
||||||
|
positionHeight: number,
|
||||||
|
) {
|
||||||
|
return isInOrNearViewport(calculateViewerAssetViewportProximity(timelineManager, positionTop, positionHeight));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,12 @@
|
|||||||
import { AssetOrder, AssetOrderBy } from '@immich/sdk';
|
import { AssetOrder, AssetOrderBy } from '@immich/sdk';
|
||||||
import { SvelteSet } from 'svelte/reactivity';
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import type { CommonLayoutOptions, CommonPosition } from '$lib/utils/layout-utils';
|
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
|
||||||
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
||||||
import { getOrderingDate, plainDateTimeCompare } from '$lib/utils/timeline-util';
|
import { getOrderingDate, plainDateTimeCompare } from '$lib/utils/timeline-util';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
|
||||||
import type { TimelineMonth } from './timeline-month.svelte';
|
import type { TimelineMonth } from './timeline-month.svelte';
|
||||||
import type { Direction, MoveAsset, TimelineAsset } from './types';
|
import type { Direction, MoveAsset, TimelineAsset } from './types';
|
||||||
import { ViewerAsset } from './viewer-asset.svelte';
|
import { ViewerAsset } from './viewer-asset.svelte';
|
||||||
|
|
||||||
const {
|
|
||||||
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
|
||||||
} = TUNABLES;
|
|
||||||
|
|
||||||
function lowerBound(assets: ViewerAsset[], target: number, key: (pos: CommonPosition) => number): number {
|
|
||||||
let lo = 0;
|
|
||||||
let hi = assets.length;
|
|
||||||
while (lo < hi) {
|
|
||||||
const mid = Math.floor((lo + hi) / 2);
|
|
||||||
if (key(assets[mid].position!) < target) {
|
|
||||||
lo = mid + 1;
|
|
||||||
} else {
|
|
||||||
hi = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TimelineDay {
|
export class TimelineDay {
|
||||||
readonly timelineMonth: TimelineMonth;
|
readonly timelineMonth: TimelineMonth;
|
||||||
readonly index: number;
|
readonly index: number;
|
||||||
@@ -37,15 +18,12 @@ export class TimelineDay {
|
|||||||
height = $state(0);
|
height = $state(0);
|
||||||
width = $state(0);
|
width = $state(0);
|
||||||
|
|
||||||
// Assets in or near the viewport; active assets should be added to the DOM.
|
|
||||||
activeViewerAssets: ViewerAsset[] = $state([]);
|
|
||||||
isInOrNearViewport = $state(false);
|
|
||||||
|
|
||||||
#top: number = $state(0);
|
#top: number = $state(0);
|
||||||
#start: number = $state(0);
|
#start: number = $state(0);
|
||||||
#row = $state(0);
|
#row = $state(0);
|
||||||
#col = $state(0);
|
#col = $state(0);
|
||||||
#deferredLayout = false;
|
#deferredLayout = false;
|
||||||
|
#lastInOrNearViewport = -1;
|
||||||
|
|
||||||
constructor(timelineMonth: TimelineMonth, index: number, day: number, groupTitle: string, orderBy: AssetOrderBy) {
|
constructor(timelineMonth: TimelineMonth, index: number, day: number, groupTitle: string, orderBy: AssetOrderBy) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
@@ -171,32 +149,18 @@ export class TimelineDay {
|
|||||||
for (let i = 0; i < this.viewerAssets.length; i++) {
|
for (let i = 0; i < this.viewerAssets.length; i++) {
|
||||||
this.viewerAssets[i].position = geometry.getPosition(i);
|
this.viewerAssets[i].position = geometry.getPosition(i);
|
||||||
}
|
}
|
||||||
this.updateAssetBoundaries();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAssetBoundaries() {
|
|
||||||
const manager = this.timelineMonth.timelineManager;
|
|
||||||
const visibleWindow = manager.visibleWindow;
|
|
||||||
if (this.viewerAssets.length === 0 || !this.viewerAssets[0].position) {
|
|
||||||
this.activeViewerAssets = [];
|
|
||||||
this.isInOrNearViewport = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dayOffset = this.absoluteTimelineDayTop;
|
|
||||||
const headerHeight = manager.headerHeight;
|
|
||||||
const expandedTop = visibleWindow.top - headerHeight - INTERSECTION_EXPAND_TOP - dayOffset;
|
|
||||||
const expandedBottom = visibleWindow.bottom + headerHeight + INTERSECTION_EXPAND_BOTTOM - dayOffset;
|
|
||||||
|
|
||||||
const first = lowerBound(this.viewerAssets, expandedTop, (p) => p.top + p.height);
|
|
||||||
const last = lowerBound(this.viewerAssets, expandedBottom, (p) => p.top) - 1;
|
|
||||||
|
|
||||||
const hasActive = last >= first && first < this.viewerAssets.length;
|
|
||||||
this.activeViewerAssets = hasActive ? this.viewerAssets.slice(first, last + 1) : [];
|
|
||||||
this.isInOrNearViewport = hasActive;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get absoluteTimelineDayTop() {
|
get absoluteTimelineDayTop() {
|
||||||
return this.timelineMonth.top + this.#top;
|
return this.timelineMonth.top + this.#top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isInOrNearViewport() {
|
||||||
|
if (this.#lastInOrNearViewport !== -1 && this.viewerAssets[this.#lastInOrNearViewport].isInOrNearViewport) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#lastInOrNearViewport = this.viewerAssets.findIndex((viewAsset) => viewAsset.isInOrNearViewport);
|
||||||
|
return this.#lastInOrNearViewport !== -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,11 +214,6 @@ export class TimelineManager extends VirtualScrollManager {
|
|||||||
|
|
||||||
for (const month of this.months) {
|
for (const month of this.months) {
|
||||||
updateTimelineMonthViewportProximity(this, month);
|
updateTimelineMonthViewportProximity(this, month);
|
||||||
if (month.isInOrNearViewport && month.isLoaded) {
|
|
||||||
for (const day of month.timelineDays) {
|
|
||||||
day.updateAssetBoundaries();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const month = this.months.find((month) => month.isInViewport);
|
const month = this.months.find((month) => month.isInViewport);
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './typ
|
|||||||
import { ViewerAsset } from './viewer-asset.svelte';
|
import { ViewerAsset } from './viewer-asset.svelte';
|
||||||
|
|
||||||
export class TimelineMonth {
|
export class TimelineMonth {
|
||||||
#viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport);
|
#isInOrNearViewport = $state(false);
|
||||||
|
#isInViewport = $state(false);
|
||||||
isLoaded: boolean = $state(false);
|
isLoaded: boolean = $state(false);
|
||||||
timelineDays: TimelineDay[] = $state([]);
|
timelineDays: TimelineDay[] = $state([]);
|
||||||
readonly timelineManager: TimelineManager;
|
readonly timelineManager: TimelineManager;
|
||||||
@@ -85,24 +86,28 @@ export class TimelineMonth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set viewportProximity(newValue: ViewportProximity) {
|
set viewportProximity(newValue: ViewportProximity) {
|
||||||
const old = this.#viewportProximity;
|
const isInOrNearViewport = isInOrNearViewportUtil(newValue);
|
||||||
if (old === newValue) {
|
if (this.#isInOrNearViewport !== isInOrNearViewport) {
|
||||||
return;
|
this.#isInOrNearViewport = isInOrNearViewport;
|
||||||
|
if (isInOrNearViewport) {
|
||||||
|
void this.timelineManager.loadTimelineMonth(this.yearMonth);
|
||||||
|
} else {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.#viewportProximity = newValue;
|
|
||||||
if (isInOrNearViewportUtil(newValue)) {
|
const isInViewport = isInViewportUtil(newValue);
|
||||||
void this.timelineManager.loadTimelineMonth(this.yearMonth);
|
if (this.#isInViewport !== isInViewport) {
|
||||||
} else {
|
this.#isInViewport = isInViewport;
|
||||||
this.cancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInOrNearViewport() {
|
get isInOrNearViewport() {
|
||||||
return isInOrNearViewportUtil(this.#viewportProximity);
|
return this.#isInOrNearViewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInViewport() {
|
get isInViewport() {
|
||||||
return isInViewportUtil(this.#viewportProximity);
|
return this.#isInViewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastTimelineDay() {
|
get lastTimelineDay() {
|
||||||
@@ -254,7 +259,7 @@ export class TimelineMonth {
|
|||||||
addContext.newTimelineDays.add(timelineDay);
|
addContext.newTimelineDays.add(timelineDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewerAsset = new ViewerAsset(timelineAsset);
|
const viewerAsset = new ViewerAsset(timelineDay, timelineAsset);
|
||||||
timelineDay.viewerAssets.push(viewerAsset);
|
timelineDay.viewerAssets.push(viewerAsset);
|
||||||
addContext.changedTimelineDays.add(timelineDay);
|
addContext.changedTimelineDays.add(timelineDay);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
import type { CommonPosition } from '$lib/utils/layout-utils';
|
import type { CommonPosition } from '$lib/utils/layout-utils';
|
||||||
|
import { calculateViewerAssetIsInOrNearViewport } from './internal/intersection-support.svelte';
|
||||||
|
import type { TimelineDay } from './timeline-day.svelte';
|
||||||
import type { TimelineAsset } from './types';
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
export class ViewerAsset {
|
export class ViewerAsset {
|
||||||
|
readonly #group: TimelineDay;
|
||||||
|
|
||||||
|
#isInOrNearViewport = $derived.by(() => {
|
||||||
|
if (!this.position) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = this.#group.timelineMonth.timelineManager;
|
||||||
|
const positionTop = this.#group.absoluteTimelineDayTop + this.position.top;
|
||||||
|
|
||||||
|
return calculateViewerAssetIsInOrNearViewport(store, positionTop, this.position.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
get isInOrNearViewport() {
|
||||||
|
return this.#isInOrNearViewport;
|
||||||
|
}
|
||||||
|
|
||||||
position: CommonPosition | undefined = $state.raw();
|
position: CommonPosition | undefined = $state.raw();
|
||||||
asset: TimelineAsset = $state() as TimelineAsset;
|
asset: TimelineAsset = $state() as TimelineAsset;
|
||||||
id: string = $derived(this.asset.id);
|
id: string = $derived(this.asset.id);
|
||||||
|
|
||||||
constructor(asset: TimelineAsset) {
|
constructor(group: TimelineDay, asset: TimelineAsset) {
|
||||||
|
this.#group = group;
|
||||||
this.asset = asset;
|
this.asset = asset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
type MaintenanceStatusResponseDto,
|
type MaintenanceStatusResponseDto,
|
||||||
type NotificationDto,
|
type NotificationDto,
|
||||||
type ReleaseEventV1,
|
|
||||||
type ServerVersionResponseDto,
|
type ServerVersionResponseDto,
|
||||||
type SyncAssetEditV1,
|
type SyncAssetEditV1,
|
||||||
type SyncAssetV2,
|
type SyncAssetV2,
|
||||||
@@ -16,6 +15,7 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
|||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { maintenanceStore } from '$lib/stores/maintenance.store';
|
import { maintenanceStore } from '$lib/stores/maintenance.store';
|
||||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||||
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
import { createEventEmitter } from '$lib/utils/eventemitter';
|
import { createEventEmitter } from '$lib/utils/eventemitter';
|
||||||
|
|
||||||
interface AppRestartEvent {
|
interface AppRestartEvent {
|
||||||
@@ -34,7 +34,7 @@ export interface Events {
|
|||||||
on_person_thumbnail: (personId: string) => void;
|
on_person_thumbnail: (personId: string) => void;
|
||||||
on_server_version: (serverVersion: ServerVersionResponseDto) => void;
|
on_server_version: (serverVersion: ServerVersionResponseDto) => void;
|
||||||
on_config_update: () => void;
|
on_config_update: () => void;
|
||||||
on_new_release: (event: ReleaseEventV1) => void;
|
on_new_release: (event: ReleaseEvent) => void;
|
||||||
on_session_delete: (sessionId: string) => void;
|
on_session_delete: (sessionId: string) => void;
|
||||||
on_notification: (notification: NotificationDto) => void;
|
on_notification: (notification: NotificationDto) => void;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { QueueResponseDto } from '@immich/sdk';
|
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||||
import type { ActionItem } from '@immich/ui';
|
import type { ActionItem } from '@immich/ui';
|
||||||
import type { DateTime } from 'luxon';
|
import type { DateTime } from 'luxon';
|
||||||
import type { SvelteSet } from 'svelte/reactivity';
|
import type { SvelteSet } from 'svelte/reactivity';
|
||||||
@@ -7,6 +7,14 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|||||||
|
|
||||||
export type LatLng = { lng: number; lat: number };
|
export type LatLng = { lng: number; lat: number };
|
||||||
|
|
||||||
|
export interface ReleaseEvent {
|
||||||
|
isAvailable: boolean;
|
||||||
|
/** ISO8601 */
|
||||||
|
checkedAt: string;
|
||||||
|
serverVersion: ServerVersionResponseDto;
|
||||||
|
releaseVersion: ServerVersionResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
||||||
|
|
||||||
export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
|
export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AssetTypeEnum } from '@immich/sdk';
|
import { AssetTypeEnum } from '@immich/sdk';
|
||||||
import { getAssetUrl } from '$lib/utils';
|
import { getAssetUrl, getReleaseType } from '$lib/utils';
|
||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||||
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
||||||
|
|
||||||
@@ -161,4 +161,26 @@ describe('utils', () => {
|
|||||||
expect(url).toContain(asset.id);
|
expect(url).toContain(asset.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(getReleaseType.name, () => {
|
||||||
|
it('should return "major" for major version changes', () => {
|
||||||
|
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 2, minor: 0, patch: 0 })).toBe('major');
|
||||||
|
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 3, minor: 2, patch: 1 })).toBe('major');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "minor" for minor version changes', () => {
|
||||||
|
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 1, patch: 0 })).toBe('minor');
|
||||||
|
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 2, patch: 1 })).toBe('minor');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "patch" for patch version changes', () => {
|
||||||
|
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 1 })).toBe('patch');
|
||||||
|
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 5 })).toBe('patch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "none" for matching versions', () => {
|
||||||
|
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 })).toBe('none');
|
||||||
|
expect(getReleaseType({ major: 1, minor: 2, patch: 3 }, { major: 1, minor: 2, patch: 3 })).toBe('none');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+20
-2
@@ -411,8 +411,26 @@ export function createDateFormatter(localeCode: string | undefined): DateFormatt
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const semverToName = ({ major, minor, patch, prerelease }: ServerVersionResponseDto) =>
|
export const getReleaseType = (
|
||||||
`v${major}.${minor}.${patch}${prerelease ? `-rc.${prerelease}` : ''}`;
|
current: ServerVersionResponseDto,
|
||||||
|
newVersion: ServerVersionResponseDto,
|
||||||
|
): 'major' | 'minor' | 'patch' | 'none' => {
|
||||||
|
if (current.major !== newVersion.major) {
|
||||||
|
return 'major';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.minor !== newVersion.minor) {
|
||||||
|
return 'minor';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.patch !== newVersion.patch) {
|
||||||
|
return 'patch';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
|
||||||
|
|
||||||
export const withoutIcons = (actions: ActionItem[]): ActionItem[] =>
|
export const withoutIcons = (actions: ActionItem[]): ActionItem[] =>
|
||||||
actions.map((action) => ({ ...action, icon: undefined }));
|
actions.map((action) => ({ ...action, icon: undefined }));
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
||||||
import { semverToName } from '$lib/utils';
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
import { ReleaseType, type ReleaseEventV1 } from '@immich/sdk';
|
import { getReleaseType, semverToName } from '$lib/utils';
|
||||||
import { modalManager } from '@immich/ui';
|
import { modalManager } from '@immich/ui';
|
||||||
|
|
||||||
let modal = $state<{
|
let modal = $state<{
|
||||||
@@ -11,20 +11,16 @@
|
|||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const onReleaseEvent = async (release: ReleaseEventV1) => {
|
const onReleaseEvent = async (release: ReleaseEvent) => {
|
||||||
if (!release.isAvailable || !authManager.user.isAdmin) {
|
if (!release.isAvailable || !authManager.user.isAdmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseVersion = semverToName(release.releaseVersion);
|
const releaseVersion = semverToName(release.releaseVersion);
|
||||||
const serverVersion = semverToName(release.serverVersion);
|
const serverVersion = semverToName(release.serverVersion);
|
||||||
|
const type = getReleaseType(release.serverVersion, release.releaseVersion);
|
||||||
|
|
||||||
if (
|
if (type === 'none' || type === 'patch' || localStorage.getItem('appVersion') === releaseVersion) {
|
||||||
!release.type ||
|
|
||||||
release.type === ReleaseType.Patch ||
|
|
||||||
release.type === ReleaseType.Prepatch ||
|
|
||||||
localStorage.getItem('appVersion') === releaseVersion
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,8 @@
|
|||||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import SettingSelect from './SettingSelect.svelte';
|
|
||||||
import { ReleaseChannel } from '@immich/sdk';
|
|
||||||
|
|
||||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||||
const config = $derived(systemConfigManager.value);
|
|
||||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -23,20 +20,6 @@
|
|||||||
bind:checked={configToEdit.newVersionCheck.enabled}
|
bind:checked={configToEdit.newVersionCheck.enabled}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
<SettingSelect
|
|
||||||
label={$t('admin.version_check_channel')}
|
|
||||||
desc={$t('admin.version_check_channel_description')}
|
|
||||||
bind:value={configToEdit.newVersionCheck.channel}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: ReleaseChannel.Stable,
|
|
||||||
text: $t('admin.release_channel_stable'),
|
|
||||||
},
|
|
||||||
{ value: ReleaseChannel.ReleaseCandidate, text: $t('admin.release_channel_release_candidate') },
|
|
||||||
]}
|
|
||||||
isEdited={configToEdit.newVersionCheck.channel !== config.newVersionCheck.channel}
|
|
||||||
{disabled}
|
|
||||||
/>
|
|
||||||
<SettingButtonsRow bind:configToEdit keys={['newVersionCheck']} {disabled} />
|
<SettingButtonsRow bind:configToEdit keys={['newVersionCheck']} {disabled} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user