mirror of
https://github.com/immich-app/immich.git
synced 2026-07-06 12:36:55 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de6f99accd |
@@ -37,7 +37,6 @@
|
||||
<a href="readme_i18n/README_ar_JO.md">العربية</a>
|
||||
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="readme_i18n/README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="readme_i18n/README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ You can trigger a database backup manually:
|
||||
|
||||
1. Go to **Administration > Job Queues**
|
||||
2. Click **Create job** in the top right
|
||||
3. Select **Create Database Dump** and click **Confirm**
|
||||
3. Select **Create Database Backup** and click **Confirm**
|
||||
|
||||
The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit.
|
||||
|
||||
|
||||
+1
-1
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.100.0"
|
||||
wrangler = "4.106.0"
|
||||
|
||||
@@ -593,7 +593,6 @@
|
||||
"asset_viewer_settings_title": "Asset Viewer",
|
||||
"assets": "Assets",
|
||||
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
||||
"assets_added_to_album_partial_count": "Added {successCount} out of {totalCount} {totalCount, plural, one {asset} other {assets}} to the album",
|
||||
"assets_added_to_albums_count": "Added {assetTotal, plural, one {# asset} other {# assets}} to {albumTotal, plural, one {# album} other {# albums}}",
|
||||
"assets_cannot_be_added_to_album_count": "{count, plural, one {Asset} other {Assets}} cannot be added to the album",
|
||||
"assets_cannot_be_added_to_albums": "{count, plural, one {Asset} other {Assets}} cannot be added to any of the albums",
|
||||
|
||||
@@ -287,43 +287,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "11.6.0"
|
||||
version = "11.9.0"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64"]
|
||||
checksum = "sha256:2fec653ff6dadab340d1c3d2214688a7451cc471f39710839440b293ca7c53b0"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120174"
|
||||
checksum = "sha256:7981d0109fffdf8f15b838243240c8c9f9114dd4b7111d4227c1453522d75b61"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748044"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:56a78c08cf22adf29e7dacb6f7f100139731693863d20fb94a7883463a62169e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120177"
|
||||
checksum = "sha256:3622d3010d6af601190e7b63994fb0fe297e805699145aec949f4879f2672fb5"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748040"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64"]
|
||||
checksum = "sha256:74d64c1646385fb21691f32f0ab6aca1a9f5c829ba54d3cda3a24838a228e68c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120176"
|
||||
checksum = "sha256:8d987d82585453bcf260ea5d0bae346d298f1a5e71bcde8d99976521408ad895"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748039"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:7a0c463a09d912fba6b7d9eca0a946bc228ea50f3015a05c09a29e7e85a932d7"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120175"
|
||||
checksum = "sha256:655fcf2609e3145cfd7c2f05113429523706d0a618f2957eaa6c3562f6a5998b"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748037"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-arm64"]
|
||||
checksum = "sha256:87c901635a14481fb30566a3749041134ffd4317bc6fe866c345b69fdf9b6b85"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120180"
|
||||
checksum = "sha256:ca6f98a8911ec975509464026205e2b54aed74cecd36c5ae9932e5997bd7de2a"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748038"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.windows-x64"]
|
||||
checksum = "sha256:91c753435542b04859c689304fae0dd64eba6b40937cfa426a48485b712e4e9c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.6.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/445120173"
|
||||
checksum = "sha256:82c130717a59f237c49e8eaa8a7ed75c1d8d351a5b6dedeee3196b8e1b597c6e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/455748043"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
|
||||
@@ -16,7 +16,7 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
pnpm = "11.6.0"
|
||||
pnpm = "11.9.0"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
|
||||
@@ -159,6 +159,8 @@ class ShareActionButton extends ConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
if (!result.success) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
@@ -171,6 +173,7 @@ class ShareActionButton extends ConsumerWidget {
|
||||
buildContext.pop();
|
||||
});
|
||||
|
||||
// Show download progress with a "Preparing" message
|
||||
return preparingDialog;
|
||||
},
|
||||
barrierDismissible: false,
|
||||
|
||||
@@ -106,7 +106,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final recommendDeferredLoading = ref.watch(timelineStateProvider.select((s) => s.recommendDeferredLoading));
|
||||
final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||
final timelineService = ref.read(timelineServiceProvider);
|
||||
final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3);
|
||||
|
||||
@@ -119,7 +119,7 @@ class _FixedSegmentRow extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (recommendDeferredLoading) {
|
||||
if (isScrubbing) {
|
||||
return _buildPlaceholder(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
|
||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||
@@ -88,6 +89,8 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
bool _isDragging = false;
|
||||
List<_Segment> _segments = [];
|
||||
int _monthCount = 0;
|
||||
DateTime? _currentScrubberDate;
|
||||
Debouncer? _scrubberDebouncer;
|
||||
|
||||
late AnimationController _thumbAnimationController;
|
||||
Timer? _fadeOutTimer;
|
||||
@@ -142,6 +145,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
_thumbAnimationController.dispose();
|
||||
_labelAnimationController.dispose();
|
||||
_fadeOutTimer?.cancel();
|
||||
_scrubberDebouncer?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -185,6 +189,24 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onScrubberDateChanged(DateTime date) {
|
||||
if (_currentScrubberDate != date) {
|
||||
// Date changed, immediately set scrubbing to true
|
||||
_currentScrubberDate = date;
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||
|
||||
// Initialize debouncer if needed
|
||||
_scrubberDebouncer ??= Debouncer(interval: const Duration(milliseconds: 50));
|
||||
|
||||
// Debounce setting scrubbing to false
|
||||
_scrubberDebouncer!.run(() {
|
||||
if (_currentScrubberDate == date) {
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onDragStart(DragStartDetails _) {
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
@@ -215,6 +237,11 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
if (_lastLabel != label) {
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
_lastLabel = label;
|
||||
|
||||
// Notify timeline state of the new scrubber date position
|
||||
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
|
||||
_onScrubberDateChanged(nearestMonthSegment.date);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +349,13 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
||||
_isDragging = false;
|
||||
});
|
||||
|
||||
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||
|
||||
// Reset scrubber tracking when drag ends
|
||||
_currentScrubberDate = null;
|
||||
_scrubberDebouncer?.dispose();
|
||||
_scrubberDebouncer = null;
|
||||
|
||||
_resetThumbTimer();
|
||||
}
|
||||
|
||||
|
||||
@@ -50,43 +50,37 @@ class TimelineArgs {
|
||||
}
|
||||
|
||||
class TimelineState {
|
||||
final bool isScrubbing;
|
||||
final bool isScrolling;
|
||||
|
||||
/// Indicates whether the timeline is scrolling beyond some configured "high" speed,
|
||||
/// such as when programmatically scrolling to the top or a really fast user fling
|
||||
final bool recommendDeferredLoading;
|
||||
const TimelineState({this.isScrubbing = false, this.isScrolling = false});
|
||||
|
||||
const TimelineState({this.isScrolling = false, this.recommendDeferredLoading = false});
|
||||
|
||||
bool get isInteracting => isScrolling || recommendDeferredLoading;
|
||||
bool get isInteracting => isScrubbing || isScrolling;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant TimelineState other) {
|
||||
return isScrolling == other.isScrolling && recommendDeferredLoading == other.recommendDeferredLoading;
|
||||
return isScrubbing == other.isScrubbing && isScrolling == other.isScrolling;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => isScrolling.hashCode ^ recommendDeferredLoading.hashCode;
|
||||
int get hashCode => isScrubbing.hashCode ^ isScrolling.hashCode;
|
||||
|
||||
TimelineState copyWith({bool? isScrolling, bool? recommendDeferredLoading}) {
|
||||
return TimelineState(
|
||||
isScrolling: isScrolling ?? this.isScrolling,
|
||||
recommendDeferredLoading: recommendDeferredLoading ?? this.recommendDeferredLoading,
|
||||
);
|
||||
TimelineState copyWith({bool? isScrubbing, bool? isScrolling}) {
|
||||
return TimelineState(isScrubbing: isScrubbing ?? this.isScrubbing, isScrolling: isScrolling ?? this.isScrolling);
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineStateNotifier extends Notifier<TimelineState> {
|
||||
void setScrubbing(bool isScrubbing) {
|
||||
state = state.copyWith(isScrubbing: isScrubbing);
|
||||
}
|
||||
|
||||
void setScrolling(bool isScrolling) {
|
||||
state = state.copyWith(isScrolling: isScrolling);
|
||||
}
|
||||
|
||||
void setRecommendDeferredLoading(bool recommendDeferredLoading) {
|
||||
state = state.copyWith(recommendDeferredLoading: recommendDeferredLoading);
|
||||
}
|
||||
|
||||
@override
|
||||
TimelineState build() => const TimelineState(isScrolling: false, recommendDeferredLoading: false);
|
||||
TimelineState build() => const TimelineState(isScrubbing: false, isScrolling: false);
|
||||
}
|
||||
|
||||
// This provider watches the buckets from the timeline service & args and serves the segments.
|
||||
|
||||
@@ -25,7 +25,6 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
|
||||
@@ -151,8 +150,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
double _baseScaleFactor = 3.0;
|
||||
int? _restoreAssetIndex;
|
||||
|
||||
final Debouncer _fastScrollDebouncer = Debouncer(interval: const Duration(milliseconds: 100));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -185,7 +182,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
switch (event) {
|
||||
case ScrollToTopEvent():
|
||||
{
|
||||
_scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut);
|
||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||
timelineState.setScrubbing(true);
|
||||
_scrollController
|
||||
.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut)
|
||||
.whenComplete(() => timelineState.setScrubbing(false));
|
||||
}
|
||||
|
||||
case ScrollToDateEvent scrollToDateEvent:
|
||||
@@ -245,31 +246,13 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fastScrollDebouncer.dispose();
|
||||
_scrollController.dispose();
|
||||
_eventSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Track whether the timeline is moving fast enough to defer per-row asset loading
|
||||
bool _onScrollVelocityNotification(ScrollNotification notification) {
|
||||
// Only consider the primary timeline ScrollView (no nested views) and update events
|
||||
if (notification.depth != 0 || notification is! ScrollUpdateNotification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use Flutter's built in fast velocity tracking
|
||||
if (_scrollController.position.recommendDeferredLoading(context)) {
|
||||
ref.read(timelineStateProvider.notifier).setRecommendDeferredLoading(true);
|
||||
|
||||
// We cannot rely on scroll end events, as the timeline scrubber jumps from position
|
||||
// to position, resulting in large spikes in velocity followed by low velocity
|
||||
_fastScrollDebouncer.run(() => ref.read(timelineStateProvider.notifier).setRecommendDeferredLoading(false));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _scrollToDate(DateTime date) {
|
||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||
asyncSegments.whenData((segments) {
|
||||
// Find the segment that contains assets from the target date
|
||||
@@ -296,11 +279,16 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
if (fallbackSegment != null) {
|
||||
// Scroll to the segment with a small offset to show the header
|
||||
final targetOffset = fallbackSegment.startOffset - 50;
|
||||
_scrollController.animateTo(
|
||||
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
timelineState.setScrubbing(true);
|
||||
_scrollController
|
||||
.animateTo(
|
||||
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
.whenComplete(() => timelineState.setScrubbing(false));
|
||||
} else {
|
||||
timelineState.setScrubbing(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -492,10 +480,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
NotificationListener<ScrollNotification>(
|
||||
onNotification: _onScrollVelocityNotification,
|
||||
child: timeline,
|
||||
),
|
||||
timeline,
|
||||
if (isBottomWidgetVisible)
|
||||
Positioned(
|
||||
top: MediaQuery.paddingOf(context).top,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"format": "prettier --cache --check i18n/",
|
||||
"format:fix": "prettier --cache --write --list-different i18n"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0"
|
||||
"packageManager": "pnpm@11.9.0"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"packageManager": "pnpm@11.6.0",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"packageManager": "pnpm@11.6.0"
|
||||
"packageManager": "pnpm@11.9.0"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
<p align="center">
|
||||
<br/>
|
||||
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
|
||||
<a href="https://discord.immich.app">
|
||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
|
||||
</a>
|
||||
<br/>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Immich">
|
||||
</p>
|
||||
<h3 align="center">ഫോട്ടോകളും വീഡിയോകളും കൈകാര്യം ചെയ്യുന്നതിനുള്ള ഉയർന്ന കാര്യക്ഷമതയുള്ള സെൽഫ്-ഹോസ്റ്റഡ് ആപ്ലിക്കേഷൻ</h3>
|
||||
<br/>
|
||||
<a href="https://immich.app">
|
||||
<img src="../design/immich-screenshots.png" title="പ്രധാന സ്ക്രീൻഷോട്ട്">
|
||||
</a>
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<a href="../README.md">English</a>
|
||||
<a href="README_ca_ES.md">Català</a>
|
||||
<a href="README_es_ES.md">Español</a>
|
||||
<a href="README_fr_FR.md">Français</a>
|
||||
<a href="README_it_IT.md">Italiano</a>
|
||||
<a href="README_ja_JP.md">日本語</a>
|
||||
<a href="README_ko_KR.md">한국어</a>
|
||||
<a href="README_de_DE.md">Deutsch</a>
|
||||
<a href="README_nl_NL.md">Nederlands</a>
|
||||
<a href="README_tr_TR.md">Türkçe</a>
|
||||
<a href="README_zh_CN.md">简体中文</a>
|
||||
<a href="README_zh_TW.md">正體中文</a>
|
||||
<a href="README_uk_UA.md">Українська</a>
|
||||
<a href="README_ru_RU.md">Русский</a>
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
</p>
|
||||
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ നിങ്ങളുടെ വിലയേറിയ ഫോട്ടോകൾക്കും വീഡിയോകൾക്കും എല്ലായ്പ്പോഴും [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) ബാക്കപ്പ് പ്ലാൻ പിന്തുടരുക!
|
||||
>
|
||||
|
||||
> [!NOTE]
|
||||
> ഇൻസ്റ്റാളേഷൻ ഗൈഡുകൾ ഉൾപ്പെടെയുള്ള പ്രധാന ഡോക്യുമെന്റേഷൻ https://immich.app/ എന്ന വെബ്സൈറ്റിൽ നിങ്ങൾക്ക് കണ്ടെത്താം.
|
||||
|
||||
## ലിങ്കുകൾ
|
||||
|
||||
- [ഡോക്യുമെന്റേഷൻ](https://docs.immich.app/)
|
||||
- [വിവരങ്ങൾ](https://docs.immich.app/overview/introduction)
|
||||
- [ഇൻസ്റ്റാളേഷൻ](https://docs.immich.app/install/requirements)
|
||||
- [റോഡ്മാപ്പ്](https://immich.app/roadmap)
|
||||
- [ഡെമോ](#ഡെമോ)
|
||||
- [സവിശേഷതകൾ](#സവിശേഷതകൾ)
|
||||
- [വിവർത്തനങ്ങൾ](https://docs.immich.app/developer/translations)
|
||||
- [സംഭാവന നൽകൽ](https://docs.immich.app/overview/support-the-project)
|
||||
|
||||
## ഡെമോ
|
||||
|
||||
ഡെമോ വെബ്സൈറ്റ് [ഇവിടെ](https://demo.immich.app) ആക്സസ് ചെയ്യാം. മൊബൈൽ ആപ്പിന്റെ ഡെമോ കാണാൻ ആപ്പ് ഇൻസ്റ്റാൾ ചെയ്ത ശേഷം, `Server Endpoint URL` എന്ന സെറ്റിങ്ങിൽ നിങ്ങൾക്ക് `https://demo.immich.app` ഉപയോഗിക്കാം.
|
||||
|
||||
### ലോഗിൻ വിവരങ്ങൾ
|
||||
|
||||
| ഇമെയിൽ | പാസ്വേഡ് |
|
||||
| --------------- | -------- |
|
||||
| demo@immich.app | demo |
|
||||
|
||||
## സവിശേഷതകൾ
|
||||
|
||||
| സവിശേഷതകൾ | മൊബൈൽ | വെബ് |
|
||||
| :-------------------------------------------------- | ------ | --- |
|
||||
| ഫോട്ടോകളും വീഡിയോകളും അപ്ലോഡ് ചെയ്യാനും കാണാനും സാധിക്കും | ഉണ്ട് | ഉണ്ട് |
|
||||
| ആപ്പ് തുറക്കുമ്പോൾ ഓട്ടോമാറ്റിക് ബാക്കപ്പ് | ഉണ്ട് | N/A |
|
||||
| ഫയലുകളുടെ ഇരട്ടിപ്പ് (duplication) തടയുന്നു | ഉണ്ട് | ഉണ്ട് |
|
||||
| ബാക്കപ്പിനായി നിർദ്ദിഷ്ട ആൽബങ്ങൾ തിരഞ്ഞെടുക്കാം | ഉണ്ട് | N/A |
|
||||
| ഫോട്ടോകളും വീഡിയോകളും ലോക്കൽ ഡിവൈസിലേക്ക് ഡൗൺലോഡ് ചെയ്യാം | ഉണ്ട് | ഉണ്ട് |
|
||||
| ഒന്നിലധികം ഉപയോക്താക്കൾക്കുള്ള പിന്തുണ (Multi-user) | ഉണ്ട് | ഉണ്ട് |
|
||||
| ആൽബങ്ങളും പങ്കിട്ട ആൽബങ്ങളും | ഉണ്ട് | ഉണ്ട് |
|
||||
| സ്ക്രബ്ബ് ചെയ്യാവുന്ന/വലിച്ചിഴക്കാവുന്ന സ്ക്രോൾബാർ | ഉണ്ട് | ഉണ്ട് |
|
||||
| റോ (RAW) ഫോർമാറ്റുകൾക്കുള്ള പിന്തുണ | ഉണ്ട് | ഉണ്ട് |
|
||||
| മെറ്റാഡാറ്റ കാഴ്ച (EXIF, മാപ്പ്) | ഉണ്ട് | ഉണ്ട് |
|
||||
| മെറ്റാഡാറ്റ, ഒബ്ജക്റ്റുകൾ, മുഖങ്ങൾ, CLIP എന്നിവ ഉപയോഗിച്ച് തിരയാം | ഉണ്ട് | ഉണ്ട് |
|
||||
| അഡ്മിനിസ്ട്രേറ്റീവ് പ്രവർത്തനങ്ങൾ (യൂസർ മാനേജ്മെന്റ്) | ഇല്ല | ഉണ്ട് |
|
||||
| ബാക്ക്ഗ്രൗണ്ട് ബാക്കപ്പ് | ഉണ്ട് | N/A |
|
||||
| വിർച്വൽ സ്ക്രോൾ | ഉണ്ട് | ഉണ്ട് |
|
||||
| OAuth സപ്പോർട്ട് | ഉണ്ട് | ഉണ്ട് |
|
||||
| API കീകൾ | N/A | ഉണ്ട് |
|
||||
| ലൈവ് ഫോട്ടോ/മോഷൻ ഫോട്ടോ ബാക്കപ്പും പ്ലേബാക്കും | ഉണ്ട് | ഉണ്ട് |
|
||||
| 360 ഡിഗ്രി ഇമേജ് ഡിസ്പ്ലേ പിന്തുണ | ഇല്ല | ഉണ്ട് |
|
||||
| ഉപയോക്താവ് നിർവചിക്കുന്ന സ്റ്റോറേജ് ഘടന | ഉണ്ട് | ഉണ്ട് |
|
||||
| പബ്ലിക് ഷെയറിംഗ് | ഉണ്ട് | ഉണ്ട് |
|
||||
| ആർക്കൈവും പ്രിയപ്പെട്ടവയും (Favorites) | ഉണ്ട് | ഉണ്ട് |
|
||||
| ആഗോള മാപ്പ് (Global Map) | ഉണ്ട് | ഉണ്ട് |
|
||||
| പങ്കാളി പങ്കിടൽ (Partner Sharing) | ഉണ്ട് | ഉണ്ട് |
|
||||
| മുഖം തിരിച്ചറിയലും ക്ലസ്റ്ററിംഗും | ഉണ്ട് | ഉണ്ട് |
|
||||
| ഓർമ്മകൾ (Memories - x വർഷങ്ങൾക്ക് മുമ്പ്) | ഉണ്ട് | ഉണ്ട് |
|
||||
| ഓഫ്ലൈൻ പിന്തുണ | ഉണ്ട് | ഇല്ല |
|
||||
| റീഡ്-ഒൺലി ഗാലറി | ഉണ്ട് | ഉണ്ട് |
|
||||
| അടുക്കിവെച്ച ഫോട്ടോകൾ (Stacked Photos) | ഉണ്ട് | ഉണ്ട് |
|
||||
| ടാഗുകൾ | ഇല്ല | ഉണ്ട് |
|
||||
| ഫോൾഡർ കാഴ്ച | ഉണ്ട് | ഉണ്ട് |
|
||||
|
||||
## വിവർത്തനങ്ങൾ
|
||||
|
||||
വിവർത്തനങ്ങളെക്കുറിച്ച് കൂടുതൽ [ഇവിടെ](https://docs.immich.app/developer/translations) വായിക്കാം.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/immich/">
|
||||
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## റെപ്പോസിറ്ററി പ്രവർത്തനം
|
||||
|
||||

|
||||
|
||||
## സ്റ്റാർ ചരിത്രം
|
||||
|
||||
<a href="https://star-history.com/#immich-app/immich&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=date" width="100%" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## സംഭാവന ചെയ്തവർ
|
||||
|
||||
<a href="https://github.com/immich-app/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
@@ -35,7 +35,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<a href="README_pt_BR.md">Português Brasileiro</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<a href="README_sv_SE.md">Svenska</a>
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
<a href="README_ar_JO.md">العربية</a>
|
||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
||||
<a href="README_th_TH.md">ภาษาไทย</a>
|
||||
<a href="README_ml_IN.md">മലയാളം</a>
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202607061334@sha256:c07ef6510ff2ccb6ead28434ade7c7a0c08aef33d7d3466b14beccdd431e86f7
|
||||
FROM ghcr.io/immich-app/base-server-prod:202606161235@sha256:c6d59e3923f548d29a212b4dc51b6281a722cfa1da7972a009c0f3830f5762d6
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202606161235@sha256:9f88b07acc8b7bf37a1dd3d5a19193f664443eaaab4e08e9f9341414c5e4b23f AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.6.14@sha256:b8f8c20fc3308f8b1d00ccca2bc968e4e208af1c5c1069e1ad9753baa099acff /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
+2
-2
@@ -84,7 +84,7 @@
|
||||
"jose": "^6.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "0.28.17",
|
||||
"kysely": "0.29.2",
|
||||
"kysely-postgres-js": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.2",
|
||||
@@ -116,7 +116,7 @@
|
||||
"ua-parser-js": "^2.0.0",
|
||||
"uuid": "^14.0.0",
|
||||
"validator": "^13.12.0",
|
||||
"zod": "4.3.6"
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.0",
|
||||
|
||||
+2
-2
@@ -76,7 +76,7 @@
|
||||
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
|
||||
"@socket.io/component-emitter": "^3.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/enhanced-img": "^0.10.4",
|
||||
"@sveltejs/enhanced-img": "^0.11.0",
|
||||
"@sveltejs/kit": "^2.56.1",
|
||||
"@sveltejs/vite-plugin-svelte": "7.1.2",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
@@ -105,7 +105,7 @@
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"prettier-plugin-svelte": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"svelte": "5.56.3",
|
||||
"svelte": "5.56.4",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.2.4",
|
||||
|
||||
@@ -58,14 +58,5 @@ describe('i18n', () => {
|
||||
expect(getClosestAvailableLocale(['sr_Cyrl'], allLocales)).toBe('sr_Cyrl');
|
||||
expect(getClosestAvailableLocale(['zh_Hant'], allLocales)).toBe('zh_Hant');
|
||||
});
|
||||
|
||||
it('should handle language aliases', () => {
|
||||
const allLocales = ['zh-Hans', 'zh-Hant'];
|
||||
expect(getClosestAvailableLocale(['zh-CN'], allLocales)).toBe('zh-Hans');
|
||||
expect(getClosestAvailableLocale(['zh-HK'], allLocales)).toBe('zh-Hant');
|
||||
expect(getClosestAvailableLocale(['zh-MO'], allLocales)).toBe('zh-Hant');
|
||||
expect(getClosestAvailableLocale(['zh-SG'], allLocales)).toBe('zh-Hans');
|
||||
expect(getClosestAvailableLocale(['zh-TW'], allLocales)).toBe('zh-Hant');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
options={[
|
||||
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
|
||||
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
|
||||
{ label: $t('owner'), value: AlbumUserRole.Owner, disabled: true },
|
||||
{ label: $t('owner'), value: AlbumUserRole.Owner },
|
||||
{ label: $t('remove_user'), value: 'none' },
|
||||
] as SelectOption<AlbumUserRole | 'none'>[]}
|
||||
onChange={(value) => handleRoleSelect(user, value)}
|
||||
|
||||
@@ -134,13 +134,10 @@ const notifyAddToAlbum = ($t: MessageFormatter, albumId: string, assetIds: strin
|
||||
const successCount = results.filter(({ success }) => success).length;
|
||||
const duplicateCount = results.filter(({ error }) => error === 'duplicate').length;
|
||||
let description = $t('assets_cannot_be_added_to_album_count', { values: { count: assetIds.length } });
|
||||
|
||||
if (duplicateCount === assetIds.length) {
|
||||
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
||||
} else if (successCount === assetIds.length) {
|
||||
if (successCount > 0) {
|
||||
description = $t('assets_added_to_album_count', { values: { count: successCount } });
|
||||
} else if (successCount > 0) {
|
||||
description = $t('assets_added_to_album_partial_count', { values: { successCount, totalCount: assetIds.length } });
|
||||
} else if (duplicateCount > 0) {
|
||||
description = $t('assets_were_part_of_album_count', { values: { count: duplicateCount } });
|
||||
}
|
||||
|
||||
toastManager.primary(
|
||||
|
||||
@@ -13,14 +13,6 @@ export const getFormatter = async () => {
|
||||
return get(t);
|
||||
};
|
||||
|
||||
const aliases: Record<string, string> = {
|
||||
'zh-CN': 'zh-Hans',
|
||||
'zh-HK': 'zh-Hant',
|
||||
'zh-MO': 'zh-Hant',
|
||||
'zh-SG': 'zh-Hans',
|
||||
'zh-TW': 'zh-Hant',
|
||||
};
|
||||
|
||||
const modules = import.meta.glob('$i18n/*.json');
|
||||
|
||||
const fileCodes = Object.keys(modules)
|
||||
@@ -41,9 +33,7 @@ const getSubLocales = (locale: string) => {
|
||||
|
||||
export const getClosestAvailableLocale = (locales: readonly string[], allLocales: readonly string[]) => {
|
||||
const allLocalesSet = new Set(allLocales.map((locale) => convertBCP47(locale)));
|
||||
return locales
|
||||
.map((locale) => aliases[locale] ?? locale)
|
||||
.find((locale) => getSubLocales(locale).some((subLocale) => allLocalesSet.has(subLocale)));
|
||||
return locales.find((locale) => getSubLocales(locale).some((subLocale) => allLocalesSet.has(subLocale)));
|
||||
};
|
||||
|
||||
export const getPreferredLocale = () => getClosestAvailableLocale(navigator.languages, langCodes);
|
||||
|
||||
Reference in New Issue
Block a user