resolve merge conflict

pull/1720/head
Michel Heusschen 2023-02-12 06:15:27 +01:00
parent 0a58280f3d
commit 0e00805187
83 changed files with 1733 additions and 1101 deletions

View File

@ -39,7 +39,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: "3.3.10" flutter-version: "3.7.3"
cache: true cache: true
- name: Create the Keystore - name: Create the Keystore

View File

@ -19,7 +19,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.3.10' flutter-version: '3.7.3'
- name: Install dependencies - name: Install dependencies
run: dart pub get run: dart pub get

View File

@ -49,7 +49,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.3.10' flutter-version: '3.7.3'
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test run: flutter test
@ -78,7 +78,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.3.10' flutter-version: '3.7.3'
- name: Run integration tests - name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2.27.0 uses: reactivecircus/android-emulator-runner@v2.27.0
with: with:

View File

@ -2,6 +2,11 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true" <application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"> android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

@ -7,7 +7,8 @@ void main() async {
await ImmichTestHelper.initialize(); await ImmichTestHelper.initialize();
group("Login input validation test", () { group("Login input validation test", () {
immichWidgetTest("Test leading/trailing whitespace", (tester, helper) async { immichWidgetTest("Test leading/trailing whitespace",
(tester, helper) async {
await helper.loginHelper.waitForLoginScreen(); await helper.loginHelper.waitForLoginScreen();
await helper.loginHelper.acknowledgeNewServerVersion(); await helper.loginHelper.acknowledgeNewServerVersion();
@ -17,7 +18,10 @@ void main() async {
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_leading_whitespace".tr()), findsOneWidget); expect(
find.text("login_form_err_leading_whitespace".tr()),
findsOneWidget,
);
await helper.loginHelper.enterCredentials( await helper.loginHelper.enterCredentials(
email: "demo@immich.app ", email: "demo@immich.app ",
@ -25,7 +29,10 @@ void main() async {
await tester.pump(const Duration(milliseconds: 300)); await tester.pump(const Duration(milliseconds: 300));
expect(find.text("login_form_err_trailing_whitespace".tr()), findsOneWidget); expect(
find.text("login_form_err_trailing_whitespace".tr()),
findsOneWidget,
);
}); });
immichWidgetTest("Test invalid email", (tester, helper) async { immichWidgetTest("Test invalid email", (tester, helper) async {
@ -40,6 +47,5 @@ void main() async {
expect(find.text("login_form_err_invalid_email".tr()), findsOneWidget); expect(find.text("login_form_err_invalid_email".tr()), findsOneWidget);
}); });
}); });
} }

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
platform :ios, '11.0' # platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -34,19 +34,8 @@ target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end end
# post_install do |installer|
# installer.pods_project.targets.each do |target|
# flutter_additional_ios_build_settings(target)
# end
# end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
config.build_settings['ENABLE_BITCODE'] = 'YES'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end end
end end

View File

@ -21,16 +21,18 @@ PODS:
- Flutter - Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_ios (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- photo_manager (2.0.0): - photo_manager (2.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_ios (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- sqflite (0.0.2): - sqflite (0.0.2):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
@ -52,10 +54,10 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
@ -86,14 +88,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/isar_flutter_libs/ios" :path: ".symlinks/plugins/isar_flutter_libs/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_foundation/ios"
photo_manager: photo_manager:
:path: ".symlinks/plugins/photo_manager/ios" :path: ".symlinks/plugins/photo_manager/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_ios: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_ios/ios" :path: ".symlinks/plugins/shared_preferences_foundation/ios"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios: url_launcher_ios:
@ -108,23 +110,23 @@ SPEC CHECKSUMS:
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07 PODFILE CHECKSUM: c798208781ca5116c4a3d5927d689946791f0189
COCOAPODS: 1.11.3 COCOAPODS: 1.11.3

View File

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 51; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -201,6 +201,7 @@
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
@ -237,6 +238,7 @@
}; };
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
@ -341,7 +343,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
NEW_SETTING = ""; NEW_SETTING = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -360,7 +362,8 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 85; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -425,7 +428,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
NEW_SETTING = ""; NEW_SETTING = "";
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
@ -475,7 +478,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
NEW_SETTING = ""; NEW_SETTING = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -495,7 +498,8 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 85; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -522,7 +526,8 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 85; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

File diff suppressed because one or more lines are too long

View File

@ -17,11 +17,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.46.0</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>85</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key> <key>MGLMapboxMetricsEnabledSettingShownInApp</key>
@ -93,5 +93,9 @@
</array> </array>
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>
<false/> <false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>FLTEnableImpeller</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -91,7 +91,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
children: [ children: [
Text( Text(
'Add to album', 'Add to album',
style: Theme.of(context).textTheme.headline2, style: Theme.of(context).textTheme.displayMedium,
), ),
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),

View File

@ -53,20 +53,25 @@ class AlbumThumbnailCard extends StatelessWidget {
type: ThumbnailFormat.JPEG, type: ThumbnailFormat.JPEG,
), ),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG), cacheKey:
getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.JPEG),
); );
} }
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Padding( child: Flex(
padding: const EdgeInsets.only(bottom: 32.0), direction: Axis.vertical,
children: [
Flexible(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( SizedBox(
width: cardSize,
height: cardSize,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(20),
child: album.albumThumbnailAssetId == null child: album.albumThumbnailAssetId == null
? buildEmptyThumbnail() ? buildEmptyThumbnail()
: buildAlbumThumbnail(), : buildAlbumThumbnail(),
@ -107,6 +112,8 @@ class AlbumThumbnailCard extends StatelessWidget {
], ],
), ),
), ),
],
),
); );
}, },
); );

View File

@ -34,7 +34,7 @@ class AlbumTitleTextField extends ConsumerWidget {
focusNode: albumTitleTextFieldFocusNode, focusNode: albumTitleTextFieldFocusNode,
style: TextStyle( style: TextStyle(
fontSize: 28, fontSize: 28,
color: Colors.grey[700], color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
controller: albumTitleController, controller: albumTitleController,

View File

@ -84,7 +84,7 @@ class CreateAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 200, left: 18), padding: const EdgeInsets.only(top: 200, left: 18),
child: Text( child: Text(
'create_shared_album_page_share_add_assets', 'create_shared_album_page_share_add_assets',
style: Theme.of(context).textTheme.headline2?.copyWith( style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
@ -214,7 +214,7 @@ class CreateAlbumPage extends HookConsumerWidget {
), ),
title: Text( title: Text(
'share_create_album', 'share_create_album',
style: Theme.of(context).textTheme.headline2?.copyWith( style: Theme.of(context).textTheme.displayMedium?.copyWith(
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
), ),
).tr(), ).tr(),
@ -228,7 +228,9 @@ class CreateAlbumPage extends HookConsumerWidget {
'create_shared_album_page_share'.tr(), 'create_shared_album_page_share'.tr(),
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: albumTitleController.text.isEmpty
? Theme.of(context).disabledColor
: Theme.of(context).primaryColor,
), ),
), ),
), ),

View File

@ -15,6 +15,7 @@ class LibraryPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider); final albums = ref.watch(albumProvider);
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect( useEffect(
() { () {
@ -122,9 +123,12 @@ class LibraryPage extends HookConsumerWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Colors.grey, color: isDarkMode
? const Color.fromARGB(255, 53, 53, 53)
: const Color.fromARGB(255, 203, 203, 203),
), ),
borderRadius: BorderRadius.circular(8), color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(20),
), ),
child: Center( child: Center(
child: Icon( child: Icon(
@ -168,25 +172,22 @@ class LibraryPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12.0, fontSize: 12.0,
color: Theme.of(context).brightness == Brightness.dark color: isDarkMode ? Colors.white : Colors.black,
? Colors.white
: Colors.black,
), ),
), ),
), ),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
side: BorderSide( side: BorderSide(
color: Theme.of(context).brightness == Brightness.dark color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
? Colors.grey[600]!
: Colors.grey[300]!,
), ),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
), ),
icon: Icon(
icon,
color: Theme.of(context).primaryColor,
), ),
icon: Icon(icon, color: Theme.of(context).primaryColor),
), ),
); );
} }

View File

@ -77,13 +77,13 @@ class SharingPage extends HookConsumerWidget {
child: Card( child: Card(
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // if you need this borderRadius: BorderRadius.circular(20),
side: const BorderSide( side: const BorderSide(
color: Colors.grey, color: Colors.grey,
width: 1, width: 0.5,
), ),
), ),
color: Colors.transparent, // color: Colors.transparent,
child: Padding( child: Padding(
padding: const EdgeInsets.all(18.0), padding: const EdgeInsets.all(18.0),
child: Column( child: Column(
@ -92,7 +92,7 @@ class SharingPage extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.only(left: 5.0, bottom: 5), padding: const EdgeInsets.only(left: 5.0, bottom: 5),
child: Icon( child: Icon(
Icons.offline_share_outlined, Icons.insert_photo_rounded,
size: 50, size: 50,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
), ),
@ -101,7 +101,7 @@ class SharingPage extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'sharing_page_empty_list', 'sharing_page_empty_list',
style: Theme.of(context).textTheme.headline3, style: Theme.of(context).textTheme.displaySmall,
).tr(), ).tr(),
), ),
Padding( Padding(

View File

@ -14,19 +14,24 @@ class ExifBottomSheet extends HookConsumerWidget {
const ExifBottomSheet({Key? key, required this.assetDetail}) const ExifBottomSheet({Key? key, required this.assetDetail})
: super(key: key); : super(key: key);
bool get showMap => assetDetail.latitude != null && assetDetail.longitude != null;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
buildMap() { buildMap() {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container( child: LayoutBuilder(
builder: (context, constraints) {
return Container(
height: 150, height: 150,
width: MediaQuery.of(context).size.width, width: constraints.maxWidth,
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(15)), borderRadius: BorderRadius.all(Radius.circular(15)),
), ),
child: FlutterMap( child: FlutterMap(
options: MapOptions( options: MapOptions(
interactiveFlags: InteractiveFlag.none,
center: LatLng( center: LatLng(
assetDetail.latitude ?? 0, assetDetail.latitude ?? 0,
assetDetail.longitude ?? 0, assetDetail.longitude ?? 0,
@ -61,6 +66,8 @@ class ExifBottomSheet extends HookConsumerWidget {
), ),
], ],
), ),
);
},
), ),
); );
} }
@ -91,46 +98,32 @@ class ExifBottomSheet extends HookConsumerWidget {
return text.isEmpty ? null : Text(text); return text.isEmpty ? null : Text(text);
} }
return SingleChildScrollView( buildDragHeader() {
child: Card( return Column(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
),
),
margin: const EdgeInsets.all(0),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: const [
const SizedBox(height: 12), SizedBox(height: 12),
const Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: CustomDraggingHandle(), child: CustomDraggingHandle(),
), ),
const SizedBox(height: 12), SizedBox(height: 12),
Text( ],
DateFormat('date_format'.tr()).format( );
assetDetail.createdAt.toLocal(), }
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
buildLocation() {
// Guard no lat/lng
if (!showMap) {
return Container();
}
return Column(
children: [
// Location // Location
if (assetDetail.latitude != null && assetDetail.longitude != null) Column(
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Divider(
thickness: 1,
),
Text( Text(
"exif_bottom_sheet_location", "exif_bottom_sheet_location",
style: TextStyle(fontSize: 11, color: textColor), style: TextStyle(fontSize: 11, color: textColor),
@ -146,17 +139,26 @@ class ExifBottomSheet extends HookConsumerWidget {
) )
], ],
), ),
],
);
}
buildDate() {
return Text(
DateFormat('date_format'.tr()).format(
assetDetail.createdAt.toLocal(),
), ),
// Detail style: const TextStyle(
Padding( fontWeight: FontWeight.bold,
padding: const EdgeInsets.only(top: 32.0), fontSize: 14,
child: Column( ),
);
}
buildDetail() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Divider(
thickness: 1,
color: Colors.grey[600],
),
Padding( Padding(
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Text( child: Text(
@ -194,13 +196,84 @@ class ExifBottomSheet extends HookConsumerWidget {
), ),
), ),
], ],
);
}
return SingleChildScrollView(
child: Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
), ),
), ),
const SizedBox( margin: const EdgeInsets.all(0),
height: 50, child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
// Two column
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildDragHeader(),
buildDate(),
const SizedBox(height: 32.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: showMap ? 5 : 0,
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: buildLocation(),
),
),
Flexible(
flex: 5,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: buildDetail(),
),
), ),
], ],
), ),
const SizedBox(height: 50),
],
),
);
}
// One column
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildDragHeader(),
buildDate(),
const SizedBox(height: 16.0),
if (showMap)
Divider(
thickness: 1,
color: Colors.grey[600],
),
const SizedBox(height: 16.0),
buildLocation(),
const SizedBox(height: 16.0),
Divider(
thickness: 1,
color: Colors.grey[600],
),
const SizedBox(height: 16.0),
buildDetail(),
const SizedBox(height: 50),
],
);
},
),
), ),
), ),
); );

View File

@ -31,12 +31,10 @@ class TopControlAppBar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
double iconSize = 18.0; const double iconSize = 18.0;
Widget buildFavoriteButton() { Widget buildFavoriteButton() {
return IconButton( return IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () { onPressed: () {
onFavorite(); onFavorite();
}, },
@ -60,12 +58,13 @@ class TopControlAppBar extends HookConsumerWidget {
color: Colors.grey[200], color: Colors.grey[200],
), ),
), ),
actionsIconTheme: const IconThemeData(
size: iconSize,
),
actions: [ actions: [
if (asset.isRemote) buildFavoriteButton(), if (asset.isRemote) buildFavoriteButton(),
if (asset.livePhotoVideoId != null) if (asset.livePhotoVideoId != null)
IconButton( IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () { onPressed: () {
onToggleMotionVideo(); onToggleMotionVideo();
}, },
@ -81,17 +80,13 @@ class TopControlAppBar extends HookConsumerWidget {
), ),
if (!asset.isLocal) if (!asset.isLocal)
IconButton( IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: onDownloadPressed, onPressed: onDownloadPressed,
icon: Icon( icon: Icon(
Icons.cloud_download_rounded, Icons.cloud_download_outlined,
color: Colors.grey[200], color: Colors.grey[200],
), ),
), ),
IconButton( IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () { onPressed: () {
onSharePressed(); onSharePressed();
}, },
@ -102,8 +97,6 @@ class TopControlAppBar extends HookConsumerWidget {
), ),
if (asset.isRemote) if (asset.isRemote)
IconButton( IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () { onPressed: () {
onAddToAlbumPressed(); onAddToAlbumPressed();
}, },
@ -113,8 +106,6 @@ class TopControlAppBar extends HookConsumerWidget {
), ),
), ),
IconButton( IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () { onPressed: () {
onDeletePressed(); onDeletePressed();
}, },
@ -124,13 +115,11 @@ class TopControlAppBar extends HookConsumerWidget {
), ),
), ),
IconButton( IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () { onPressed: () {
onMoreInfoPressed(); onMoreInfoPressed();
}, },
icon: Icon( icon: Icon(
Icons.more_horiz_rounded, Icons.info_outline_rounded,
color: Colors.grey[200], color: Colors.grey[200],
), ),
), ),

View File

@ -15,7 +15,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
import 'package:immich_mobile/shared/services/asset.service.dart'; import 'package:immich_mobile/shared/services/asset.service.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart'; import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
@ -213,7 +213,7 @@ class GalleryViewerPage extends HookConsumerWidget {
void handleSwipeUpDown(DragUpdateDetails details) { void handleSwipeUpDown(DragUpdateDetails details) {
int sensitivity = 15; int sensitivity = 15;
int dxThreshhold = 50; int dxThreshold = 50;
if (isZoomed.value) { if (isZoomed.value) {
return; return;
@ -222,7 +222,7 @@ class GalleryViewerPage extends HookConsumerWidget {
// Check for delta from initial down point // Check for delta from initial down point
final d = details.localPosition - localPosition; final d = details.localPosition - localPosition;
// If the magnitude of the dx swipe is large, we probably didn't mean to go down // If the magnitude of the dx swipe is large, we probably didn't mean to go down
if (d.dx.abs() > dxThreshhold) { if (d.dx.abs() > dxThreshold) {
return; return;
} }
@ -314,7 +314,7 @@ class GalleryViewerPage extends HookConsumerWidget {
? (context, event) { ? (context, event) {
final asset = assetList[indexOfAsset.value]; final asset = assetList[indexOfAsset.value];
if (!asset.isLocal) { if (!asset.isLocal) {
// Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to acheive // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
// Three-Stage Loading (WEBP -> JPEG -> Original) // Three-Stage Loading (WEBP -> JPEG -> Original)
final webPThumbnail = CachedNetworkImage( final webPThumbnail = CachedNetworkImage(
imageUrl: getThumbnailUrl( imageUrl: getThumbnailUrl(

View File

@ -81,10 +81,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: GestureDetector( child: GestureDetector(
onTap: removeSelection, onTap: removeSelection,
child: Chip( child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
label: Text( label: Text(
album.name, album.name,
style: TextStyle( style: TextStyle(
@ -119,10 +115,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: Chip( child: Chip(
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
label: Text( label: Text(
album.name, album.name,
style: TextStyle( style: TextStyle(

View File

@ -75,22 +75,23 @@ class RenderList {
RenderList(this.elements); RenderList(this.elements);
static Map<String, List<Asset>> _groupAssets( static Map<DateTime, List<Asset>> _groupAssets(
List<Asset> assets, List<Asset> assets,
GroupAssetsBy groupBy, GroupAssetsBy groupBy,
) { ) {
assets.sortByCompare<DateTime>(
(e) => e.createdAt,
(a, b) => b.compareTo(a),
);
if (groupBy == GroupAssetsBy.day) { if (groupBy == GroupAssetsBy.day) {
return assets.groupListsBy( return assets.groupListsBy(
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()), (element) {
final date = element.createdAt.toLocal();
return DateTime(date.year, date.month, date.day);
},
); );
} else if (groupBy == GroupAssetsBy.month) { } else if (groupBy == GroupAssetsBy.month) {
return assets.groupListsBy( return assets.groupListsBy(
(element) => DateFormat('y-MM').format(element.createdAt.toLocal()), (element) {
final date = element.createdAt.toLocal();
return DateTime(date.year, date.month);
},
); );
} }
@ -113,10 +114,11 @@ class RenderList {
final groups = _groupAssets(allAssets, groupBy); final groups = _groupAssets(allAssets, groupBy);
groups.forEach((groupName, assets) { groups.entries.sortedBy((e) =>e.key).reversed.forEach((entry) {
try { final date = entry.key;
final date = assets.first.createdAt.toLocal(); final assets = entry.value;
try {
// Month title // Month title
if (groupBy == GroupAssetsBy.day && if (groupBy == GroupAssetsBy.day &&
(lastDate == null || lastDate!.month != date.month)) { (lastDate == null || lastDate!.month != date.month)) {

View File

@ -92,7 +92,6 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
RenderAssetGridRow row, RenderAssetGridRow row,
bool scrolling, bool scrolling,
) { ) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final size = constraints.maxWidth / widget.assetsPerRow - final size = constraints.maxWidth / widget.assetsPerRow -
@ -141,7 +140,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
style: TextStyle( style: TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.headline1?.color, color: Theme.of(context).textTheme.displayLarge?.color,
), ),
), ),
); );
@ -164,7 +163,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
Text _labelBuilder(int pos) { Text _labelBuilder(int pos) {
final date = widget.renderList.elements[pos].date; final date = widget.renderList.elements[pos].date;
return Text( return Text(
DateFormat.yMMMd().format(date), DateFormat.yMMMM().format(date),
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@ -22,7 +22,7 @@ class MonthlyTitleText extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.headline1?.color, color: Theme.of(context).textTheme.displayLarge?.color,
), ),
), ),
), ),

View File

@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart'; import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/shared/ui/drag_sheet.dart'; import 'package:immich_mobile/shared/ui/drag_sheet.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
@ -29,6 +29,8 @@ class ControlBottomAppBar extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
Widget renderActionButtons() { Widget renderActionButtons() {
return Row( return Row(
children: [ children: [
@ -60,7 +62,6 @@ class ControlBottomAppBar extends ConsumerWidget {
); );
}, },
), ),
], ],
); );
} }
@ -75,7 +76,9 @@ class ControlBottomAppBar extends ConsumerWidget {
ScrollController scrollController, ScrollController scrollController,
) { ) {
return Card( return Card(
elevation: 12.0, color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
surfaceTintColor: Colors.transparent,
elevation: 18.0,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
@ -83,13 +86,6 @@ class ControlBottomAppBar extends ConsumerWidget {
), ),
), ),
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
child: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: CustomScrollView( child: CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: [ slivers: [
@ -122,7 +118,6 @@ class ControlBottomAppBar extends ConsumerWidget {
) )
], ],
), ),
),
); );
}, },
); );

View File

@ -1,5 +1,4 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:badges/badges.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@ -29,7 +28,6 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
final ServerInfoState serverInfoState = ref.watch(serverInfoProvider); final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
return AppBar( return AppBar(
centerTitle: true,
backgroundColor: Theme.of(context).appBarTheme.backgroundColor, backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
@ -44,10 +42,9 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
top: 5, top: 5,
child: IconButton( child: IconButton(
splashRadius: 25, splashRadius: 25,
icon: Icon( icon: const Icon(
Icons.face_outlined, Icons.face_outlined,
size: 30, size: 30,
color: Theme.of(context).primaryColor,
), ),
onPressed: () { onPressed: () {
Scaffold.of(context).openDrawer(); Scaffold.of(context).openDrawer();
@ -112,16 +109,13 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
splashRadius: 25, splashRadius: 25,
iconSize: 30, iconSize: 30,
icon: isEnableAutoBackup icon: isEnableAutoBackup
? Icon( ? const Icon(
Icons.backup_rounded, Icons.backup_rounded,
color: Theme.of(context).primaryColor,
) )
: Badge( : Badge(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
elevation: 3, backgroundColor: Colors.white,
position: BadgePosition.bottomEnd(bottom: -4, end: -4), label: const Icon(
badgeColor: Colors.white,
badgeContent: const Icon(
Icons.cloud_off_rounded, Icons.cloud_off_rounded,
size: 8, size: 8,
color: Colors.indigo, color: Colors.indigo,

View File

@ -15,7 +15,7 @@ class ProfileDrawer extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
buildSignoutButton() { buildSignOutButton() {
return ListTile( return ListTile(
horizontalTitleGap: 0, horizontalTitleGap: 0,
leading: SizedBox( leading: SizedBox(
@ -95,6 +95,9 @@ class ProfileDrawer extends HookConsumerWidget {
} }
return Drawer( return Drawer(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -105,7 +108,7 @@ class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawerHeader(), const ProfileDrawerHeader(),
buildSettingButton(), buildSettingButton(),
buildAppLogButton(), buildAppLogButton(),
buildSignoutButton(), buildSignOutButton(),
], ],
), ),
const ServerInfoBox() const ServerInfoBox()

View File

@ -22,7 +22,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
AuthenticationState authState = ref.watch(authenticationProvider); AuthenticationState authState = ref.watch(authenticationProvider);
final uploadProfileImageStatus = final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status; ref.watch(uploadProfileImageProvider).status;
var dummmy = Random().nextInt(1024); var dummy = Random().nextInt(1024);
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
buildUserProfileImage() { buildUserProfileImage() {
@ -39,7 +39,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
return CircleAvatar( return CircleAvatar(
radius: 35, radius: 35,
backgroundImage: NetworkImage( backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', '$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
), ),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
); );
@ -56,7 +56,7 @@ class ProfileDrawerHeader extends HookConsumerWidget {
return CircleAvatar( return CircleAvatar(
radius: 35, radius: 35,
backgroundImage: NetworkImage( backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}', '$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
), ),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
); );

View File

@ -53,6 +53,9 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
}, },
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'search_bar_hint'.tr(), hintText: 'search_bar_hint'.tr(),
hintStyle: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
),
enabledBorder: const UnderlineInputBorder( enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: Colors.transparent), borderSide: BorderSide(color: Colors.transparent),
), ),

View File

@ -11,7 +11,6 @@ class TabControllerPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
navigationRail(TabsRouter tabsRouter) { navigationRail(TabsRouter tabsRouter) {
return NavigationRail( return NavigationRail(
labelType: NavigationRailLabelType.all, labelType: NavigationRailLabelType.all,
@ -61,6 +60,7 @@ class TabControllerPage extends ConsumerWidget {
); );
} }
// ignore: unused_element
bottomNavigationBar(TabsRouter tabsRouter) { bottomNavigationBar(TabsRouter tabsRouter) {
return BottomNavigationBar( return BottomNavigationBar(
selectedLabelStyle: const TextStyle( selectedLabelStyle: const TextStyle(
@ -101,6 +101,58 @@ class TabControllerPage extends ConsumerWidget {
); );
} }
experimentalNavigationBar(TabsRouter tabsRouter) {
return NavigationBar(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: (index) {
HapticFeedback.selectionClick();
tabsRouter.setActiveIndex(index);
},
destinations: [
NavigationDestination(
label: 'tab_controller_nav_photos'.tr(),
icon: const Icon(
Icons.photo_outlined,
),
selectedIcon: Icon(
Icons.photo,
color: Theme.of(context).primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_search'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: Theme.of(context).primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_sharing'.tr(),
icon: const Icon(
Icons.group_outlined,
),
selectedIcon: Icon(
Icons.group,
color: Theme.of(context).primaryColor,
),
),
NavigationDestination(
label: 'tab_controller_nav_library'.tr(),
icon: const Icon(
Icons.photo_album_outlined,
),
selectedIcon: Icon(
Icons.photo_album_rounded,
color: Theme.of(context).primaryColor,
),
)
],
);
}
final multiselectEnabled = ref.watch(multiselectProvider); final multiselectEnabled = ref.watch(multiselectProvider);
return AutoTabsRouter( return AutoTabsRouter(
routes: [ routes: [
@ -127,7 +179,7 @@ class TabControllerPage extends ConsumerWidget {
final Widget body; final Widget body;
if (constraints.maxWidth < medium) { if (constraints.maxWidth < medium) {
// Normal phone width // Normal phone width
bottom = bottomNavigationBar(tabsRouter); bottom = experimentalNavigationBar(tabsRouter);
body = FadeTransition( body = FadeTransition(
opacity: animation, opacity: animation,
child: child, child: child,
@ -146,13 +198,13 @@ class TabControllerPage extends ConsumerWidget {
), ),
], ],
); );
} return Scaffold( }
return Scaffold(
body: body, body: body,
bottomNavigationBar: multiselectEnabled bottomNavigationBar: multiselectEnabled ? null : bottom,
? null
: bottom,
); );
},), },
),
); );
}, },
); );

View File

@ -20,6 +20,83 @@ final immichThemeProvider = StateProvider<ThemeMode>((ref) {
} }
}); });
ThemeData base = ThemeData(
chipTheme: const ChipThemeData(
side: BorderSide.none,
),
);
ThemeData immichLightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
primaryColor: Colors.indigo,
hintColor: Colors.indigo,
fontFamily: 'WorkSans',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
appBarTheme: AppBarTheme(
titleTextStyle: const TextStyle(
fontFamily: 'WorkSans',
color: Colors.indigo,
),
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedItemColor: Colors.indigo,
),
cardTheme: const CardTheme(
surfaceTintColor: Colors.transparent,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichBackgroundColor,
),
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
displayMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
displaySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
),
chipTheme: base.chipTheme,
popupMenuTheme: const PopupMenuThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
surfaceTintColor: Colors.transparent,
color: Colors.white,
),
navigationBarTheme: NavigationBarThemeData(
indicatorColor: Colors.indigo.withOpacity(0.15),
backgroundColor: immichBackgroundColor,
surfaceTintColor: Colors.transparent,
),
);
ThemeData immichDarkTheme = ThemeData( ThemeData immichDarkTheme = ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark, brightness: Brightness.dark,
@ -43,7 +120,8 @@ ThemeData immichDarkTheme = ThemeData(
), ),
backgroundColor: const Color.fromARGB(255, 32, 33, 35), backgroundColor: const Color.fromARGB(255, 32, 33, 35),
foregroundColor: immichDarkThemePrimaryColor, foregroundColor: immichDarkThemePrimaryColor,
elevation: 1, elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true, centerTitle: true,
), ),
bottomNavigationBarTheme: BottomNavigationBarThemeData( bottomNavigationBarTheme: BottomNavigationBarThemeData(
@ -56,17 +134,17 @@ ThemeData immichDarkTheme = ThemeData(
scrimColor: Colors.white.withOpacity(0.1), scrimColor: Colors.white.withOpacity(0.1),
), ),
textTheme: TextTheme( textTheme: TextTheme(
headline1: const TextStyle( displayLarge: const TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255), color: Color.fromARGB(255, 255, 255, 255),
), ),
headline2: const TextStyle( displayMedium: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255), color: Color.fromARGB(255, 255, 255, 255),
), ),
headline3: TextStyle( displaySmall: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: immichDarkThemePrimaryColor, color: immichDarkThemePrimaryColor,
@ -79,57 +157,19 @@ ThemeData immichDarkTheme = ThemeData(
backgroundColor: immichDarkThemePrimaryColor, backgroundColor: immichDarkThemePrimaryColor,
), ),
), ),
); chipTheme: base.chipTheme,
popupMenuTheme: const PopupMenuThemeData(
ThemeData immichLightTheme = ThemeData( shape: RoundedRectangleBorder(
useMaterial3: true, borderRadius: BorderRadius.all(Radius.circular(10)),
brightness: Brightness.light,
primarySwatch: Colors.indigo,
hintColor: Colors.indigo,
fontFamily: 'WorkSans',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
), ),
appBarTheme: AppBarTheme( surfaceTintColor: Colors.transparent,
titleTextStyle: const TextStyle(
fontFamily: 'WorkSans',
color: Colors.indigo,
), ),
backgroundColor: immichBackgroundColor, navigationBarTheme: NavigationBarThemeData(
foregroundColor: Colors.indigo, indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4),
elevation: 1, iconTheme: const MaterialStatePropertyAll(
centerTitle: true, IconThemeData(color: Colors.white),
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedItemColor: Colors.indigo,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichBackgroundColor,
),
textTheme: const TextTheme(
headline1: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
headline2: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
headline3: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
), ),
backgroundColor: Colors.grey[900],
surfaceTintColor: Colors.transparent,
), ),
); );

View File

@ -14,6 +14,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';

View File

@ -144,19 +144,19 @@ class ApiClient {
); );
} }
Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) async => Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) =>
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package
deserialize(json, targetType, growable: growable); deserialize(json, targetType, growable: growable);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
dynamic deserialize(String json, String targetType, {bool growable = false,}) { Future<dynamic> deserialize(String json, String targetType, {bool growable = false,}) async {
// Remove all spaces. Necessary for regular expressions as well. // Remove all spaces. Necessary for regular expressions as well.
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
// If the expected target type is String, nothing to do... // If the expected target type is String, nothing to do...
return targetType == 'String' return targetType == 'String'
? json ? json
: _deserialize(jsonDecode(json), targetType, growable: growable); : _deserialize(await compute((String j) => jsonDecode(j), json), targetType, growable: growable);
} }
// ignore: deprecated_member_use_from_same_package // ignore: deprecated_member_use_from_same_package

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
function mobile { function mobile {
rm -rf ../mobile/openapi rm -rf ../mobile/openapi
@ -7,6 +7,10 @@ function mobile {
patch -u native_class.mustache <native_class.mustache.patch patch -u native_class.mustache <native_class.mustache.patch
cd ../../../../.. cd ../../../../..
npx openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./openapi-generator/templates/mobile npx openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./openapi-generator/templates/mobile
# Post generate patches
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./openapi-generator/patch/api_client.dart.patch
patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./openapi-generator/patch/api.dart.patch
} }
function web { function web {

View File

@ -0,0 +1,8 @@
@@ -14,6 +14,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:intl/intl.dart';
import 'package:meta/meta.dart';

View File

@ -0,0 +1,21 @@
@@ -144,19 +144,19 @@ class ApiClient {
);
}
- Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) async =>
+ Future<dynamic> deserializeAsync(String json, String targetType, {bool growable = false,}) =>
// ignore: deprecated_member_use_from_same_package
deserialize(json, targetType, growable: growable);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
- dynamic deserialize(String json, String targetType, {bool growable = false,}) {
+ Future<dynamic> deserialize(String json, String targetType, {bool growable = false,}) async {
// Remove all spaces. Necessary for regular expressions as well.
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
// If the expected target type is String, nothing to do...
return targetType == 'String'
? json
- : _deserialize(jsonDecode(json), targetType, growable: growable);
+ : _deserialize(await compute((String j) => jsonDecode(j), json), targetType, growable: growable);
}

View File

@ -86,6 +86,8 @@ export default {
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: { moduleNameMapper: {
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'identity-obj-proxy',
'^\\$lib(.*)$': '<rootDir>/src/lib$1', '^\\$lib(.*)$': '<rootDir>/src/lib$1',
'^\\@api(.*)$': '<rootDir>/src/api$1', '^\\@api(.*)$': '<rootDir>/src/api$1',
'^\\@test-data(.*)$': '<rootDir>/src/test-data$1' '^\\@test-data(.*)$': '<rootDir>/src/test-data$1'

48
web/package-lock.json generated
View File

@ -17,7 +17,6 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"luxon": "^3.1.1", "luxon": "^3.1.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"svelte-keydown": "^0.5.0",
"svelte-material-icons": "^2.0.2" "svelte-material-icons": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {
@ -45,6 +44,7 @@
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"factory.ts": "^1.2.0", "factory.ts": "^1.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.0.2", "jest": "^29.0.2",
"jest-environment-jsdom": "^29.0.2", "jest-environment-jsdom": "^29.0.2",
"postcss": "^8.4.13", "postcss": "^8.4.13",
@ -53,7 +53,6 @@
"svelte": "^3.44.0", "svelte": "^3.44.0",
"svelte-check": "^2.7.1", "svelte-check": "^2.7.1",
"svelte-jester": "^2.3.2", "svelte-jester": "^2.3.2",
"svelte-keydown": "^0.5.0",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.0.24", "tailwindcss": "^3.0.24",
"tslib": "^2.3.1", "tslib": "^2.3.1",
@ -6202,6 +6201,12 @@
"uglify-js": "^3.1.4" "uglify-js": "^3.1.4"
} }
}, },
"node_modules/harmony-reflect": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
"integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
"dev": true
},
"node_modules/has": { "node_modules/has": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@ -6337,6 +6342,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
"dev": true,
"dependencies": {
"harmony-reflect": "^1.4.6"
},
"engines": {
"node": ">=4"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
@ -10573,12 +10590,6 @@
"svelte": ">= 3" "svelte": ">= 3"
} }
}, },
"node_modules/svelte-keydown": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/svelte-keydown/-/svelte-keydown-0.5.0.tgz",
"integrity": "sha512-DgY6AYlKbBocSvjC3kUeNPcStJQOTOCxAGG9ymVHzJdsQ1hRJuB8pcnB4UFH8uH3bAPdYyXXa3LwenLDL41eqQ==",
"dev": true
},
"node_modules/svelte-material-icons": { "node_modules/svelte-material-icons": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", "resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz",
@ -15822,6 +15833,12 @@
"wordwrap": "^1.0.0" "wordwrap": "^1.0.0"
} }
}, },
"harmony-reflect": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
"integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
"dev": true
},
"has": { "has": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@ -15918,6 +15935,15 @@
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
} }
}, },
"identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
"dev": true,
"requires": {
"harmony-reflect": "^1.4.6"
}
},
"ignore": { "ignore": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
@ -18997,12 +19023,6 @@
"dev": true, "dev": true,
"requires": {} "requires": {}
}, },
"svelte-keydown": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/svelte-keydown/-/svelte-keydown-0.5.0.tgz",
"integrity": "sha512-DgY6AYlKbBocSvjC3kUeNPcStJQOTOCxAGG9ymVHzJdsQ1hRJuB8pcnB4UFH8uH3bAPdYyXXa3LwenLDL41eqQ==",
"dev": true
},
"svelte-material-icons": { "svelte-material-icons": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", "resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz",

View File

@ -43,6 +43,7 @@
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"factory.ts": "^1.2.0", "factory.ts": "^1.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.0.2", "jest": "^29.0.2",
"jest-environment-jsdom": "^29.0.2", "jest-environment-jsdom": "^29.0.2",
"postcss": "^8.4.13", "postcss": "^8.4.13",
@ -51,7 +52,6 @@
"svelte": "^3.44.0", "svelte": "^3.44.0",
"svelte-check": "^2.7.1", "svelte-check": "^2.7.1",
"svelte-jester": "^2.3.2", "svelte-jester": "^2.3.2",
"svelte-keydown": "^0.5.0",
"svelte-preprocess": "^4.10.7", "svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.0.24", "tailwindcss": "^3.0.24",
"tslib": "^2.3.1", "tslib": "^2.3.1",
@ -69,7 +69,6 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"luxon": "^3.1.1", "luxon": "^3.1.1",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"svelte-keydown": "^0.5.0",
"svelte-material-icons": "^2.0.2" "svelte-material-icons": "^2.0.2"
} }
} }

View File

@ -4,13 +4,13 @@
@font-face { @font-face {
font-family: 'Work Sans'; font-family: 'Work Sans';
src: url('/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations'); src: url('$lib/assets/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations');
font-weight: 1 999; font-weight: 1 999;
} }
@font-face { @font-face {
font-family: 'Snowburst One'; font-family: 'Snowburst One';
src: url('/fonts/SnowburstOne-Regular.ttf') format('truetype'); src: url('$lib/assets/fonts/SnowburstOne-Regular.ttf') format('truetype');
} }
:root { :root {

View File

@ -2,7 +2,6 @@
<html lang="en" class="dark"> <html lang="en" class="dark">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 584 B

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type Icon from 'svelte-material-icons/AbTesting.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
// eslint-disable-next-line @typescript-eslint/no-explicit-any export let logo: typeof Icon;
export let logo: any;
export let title: string; export let title: string;
export let value: string; export let value: string;
export let unit: string | undefined = undefined; export let unit: string | undefined = undefined;
@ -13,13 +13,10 @@
} }
const maxLength = 13; const maxLength = 13;
let result = '';
const valueLength = parseInt(value).toString().length; const valueLength = parseInt(value).toString().length;
const zeroLength = maxLength - valueLength; const zeroLength = maxLength - valueLength;
for (let i = 0; i < zeroLength; i++) {
result += '0'; return '0'.repeat(zeroLength);
}
return result;
}; };
</script> </script>

View File

@ -44,10 +44,10 @@ describe('AlbumCard component', () => {
const albumDetailsElement = sut.getByTestId('album-details'); const albumDetailsElement = sut.getByTestId('album-details');
const detailsText = `${count} items` + (shared ? ' . Shared' : ''); const detailsText = `${count} items` + (shared ? ' . Shared' : '');
expect(albumImgElement).toHaveAttribute('src', 'no-thumbnail.png'); expect(albumImgElement).toHaveAttribute('src');
expect(albumImgElement).toHaveAttribute('alt', album.id); expect(albumImgElement).toHaveAttribute('alt', album.id);
await waitFor(() => expect(albumImgElement).toHaveAttribute('src', 'no-thumbnail.png')); await waitFor(() => expect(albumImgElement).toHaveAttribute('src'));
expect(albumImgElement).toHaveAttribute('alt', album.id); expect(albumImgElement).toHaveAttribute('alt', album.id);
expect(apiMock.assetApi.getAssetThumbnail).not.toHaveBeenCalled(); expect(apiMock.assetApi.getAssetThumbnail).not.toHaveBeenCalled();
@ -108,7 +108,7 @@ describe('AlbumCard component', () => {
sut = render(AlbumCard, { album }); sut = render(AlbumCard, { album });
const albumImgElement = sut.getByTestId('album-image'); const albumImgElement = sut.getByTestId('album-image');
await waitFor(() => expect(albumImgElement).toHaveAttribute('src', 'no-thumbnail.png')); await waitFor(() => expect(albumImgElement).toHaveAttribute('src'));
}); });
it('dispatches custom "click" event with the album in context', async () => { it('dispatches custom "click" event with the album in context', async () => {

View File

@ -16,14 +16,13 @@
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
import CircleIconButton from '../shared-components/circle-icon-button.svelte'; import CircleIconButton from '../shared-components/circle-icon-button.svelte';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
const NO_THUMBNAIL = 'no-thumbnail.png';
let imageData = `/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`; let imageData = `/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`;
if (!album.albumThumbnailAssetId) { if (!album.albumThumbnailAssetId) {
imageData = NO_THUMBNAIL; imageData = noThumbnailUrl;
} }
const dispatchClick = createEventDispatcher<OnClick>(); const dispatchClick = createEventDispatcher<OnClick>();
@ -51,7 +50,7 @@
}; };
onMount(async () => { onMount(async () => {
imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || NO_THUMBNAIL; imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || noThumbnailUrl;
}); });
const locale = navigator.language; const locale = navigator.language;

View File

@ -40,6 +40,7 @@
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { bulkDownload } from '$lib/utils/asset-utils'; import { bulkDownload } from '$lib/utils/asset-utils';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let sharedLink: SharedLinkResponseDto | undefined = undefined; export let sharedLink: SharedLinkResponseDto | undefined = undefined;
@ -329,12 +330,8 @@
} }
}; };
const showAlbumOptionsMenu = (event: CustomEvent) => { const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => {
contextMenuPosition = { contextMenuPosition = { x, y };
x: event.detail.mouseEvent.x,
y: event.detail.mouseEvent.y
};
isShowAlbumOptions = !isShowAlbumOptions; isShowAlbumOptions = !isShowAlbumOptions;
}; };
@ -419,13 +416,7 @@
class="flex gap-2 place-items-center hover:cursor-pointer ml-6" class="flex gap-2 place-items-center hover:cursor-pointer ml-6"
href="https://immich.app" href="https://immich.app"
> >
<img <ImmichLogo height={30} width={30} />
src="/immich-logo.svg"
alt="immich logo"
height="30"
width="30"
draggable="false"
/>
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary"> <h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">
IMMICH IMMICH
</h1> </h1>
@ -472,7 +463,7 @@
{#if !isPublicShared} {#if !isPublicShared}
<CircleIconButton <CircleIconButton
title="Album options" title="Album options"
on:click={(event) => showAlbumOptionsMenu(event)} on:click={showAlbumOptionsMenu}
logo={DotsVertical} logo={DotsVertical}
/> />
{/if} {/if}

View File

@ -6,6 +6,7 @@
import Link from 'svelte-material-icons/Link.svelte'; import Link from 'svelte-material-icons/Link.svelte';
import ShareCircle from 'svelte-material-icons/ShareCircle.svelte'; import ShareCircle from 'svelte-material-icons/ShareCircle.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ImmichLogo from '../shared-components/immich-logo.svelte';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let sharedUsersInAlbum: Set<UserResponseDto>; export let sharedUsersInAlbum: Set<UserResponseDto>;
@ -53,7 +54,7 @@
<BaseModal on:close={() => dispatch('close')}> <BaseModal on:close={() => dispatch('close')}>
<svelte:fragment slot="title"> <svelte:fragment slot="title">
<span class="flex gap-2 place-items-center"> <span class="flex gap-2 place-items-center">
<img src="/immich-logo.svg" width="24" alt="Immich" draggable="false" /> <ImmichLogo width={24} />
<p class="font-medium">Invite to album</p> <p class="font-medium">Invite to album</p>
</span> </span>
</svelte:fragment> </svelte:fragment>

View File

@ -31,12 +31,8 @@
let contextMenuPosition = { x: 0, y: 0 }; let contextMenuPosition = { x: 0, y: 0 };
let isShowAssetOptions = false; let isShowAssetOptions = false;
const showOptionsMenu = (event: CustomEvent) => { const showOptionsMenu = ({ x, y }: MouseEvent) => {
contextMenuPosition = { contextMenuPosition = { x, y };
x: event.detail.mouseEvent.x,
y: event.detail.mouseEvent.y
};
isShowAssetOptions = !isShowAssetOptions; isShowAssetOptions = !isShowAssetOptions;
}; };
@ -101,11 +97,7 @@
{#if isOwner} {#if isOwner}
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" /> <CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
<CircleIconButton <CircleIconButton logo={DotsVertical} on:click={showOptionsMenu} title="More" />
logo={DotsVertical}
on:click={(event) => showOptionsMenu(event)}
title="More"
/>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -304,7 +304,7 @@
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
/> />
{:else} {:else}
<PhotoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} /> <PhotoViewer {publicSharedKey} {asset} on:close={closeViewer} />
{/if} {/if}
{:else} {:else}
<VideoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} /> <VideoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} />

View File

@ -1,39 +1,21 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { onMount } from 'svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { api, AssetResponseDto } from '@api'; import { api, AssetResponseDto } from '@api';
import Keydown from 'svelte-keydown'; import { copyImageToClipboard } from 'copy-image-clipboard';
import { import {
notificationController, notificationController,
NotificationType NotificationType
} from '../shared-components/notification/notification'; } from '../shared-components/notification/notification';
export let assetId: string; export let asset: AssetResponseDto;
export let publicSharedKey = ''; export let publicSharedKey = '';
let assetInfo: AssetResponseDto;
let assetData: string; let assetData: string;
let copyImageToClipboard: (src: string) => Promise<Blob>;
onMount(async () => {
const { data } = await api.assetApi.getAssetById(assetId, {
params: {
key: publicSharedKey
}
});
assetInfo = data;
//Import hack :( see https://github.com/vadimkorr/svelte-carousel/issues/27#issuecomment-851022295
const module = await import('copy-image-clipboard');
copyImageToClipboard = module.copyImageToClipboard;
});
const loadAssetData = async () => { const loadAssetData = async () => {
try { try {
const { data } = await api.assetApi.serveFile(assetInfo.id, false, true, { const { data } = await api.assetApi.serveFile(asset.id, false, true, {
params: { params: {
key: publicSharedKey key: publicSharedKey
}, },
@ -51,42 +33,51 @@
} }
}; };
const handleKeypress = async (keyEvent: CustomEvent<string>) => { const handleKeypress = async ({ metaKey, ctrlKey, key }: KeyboardEvent) => {
if (keyEvent.detail == 'Control-c' || keyEvent.detail == 'Meta-c') { if ((metaKey || ctrlKey) && key === 'c') {
await doCopy(); await doCopy();
} }
}; };
export const doCopy = async () => { export const doCopy = async () => {
try {
await copyImageToClipboard(assetData); await copyImageToClipboard(assetData);
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: 'Copied image to clipboard.', message: 'Copied image to clipboard.',
timeout: 3000 timeout: 3000
}); });
} catch (err) {
console.error(err);
notificationController.show({
type: NotificationType.Error,
message: 'Copying image to clipboard failed. Click here to learn more.',
timeout: 5000,
action: {
type: 'link',
target:
'https://github.com/LuanEdCosta/copy-image-clipboard#enable-clipboard-api-features-in-firefox'
}
});
}
}; };
</script> </script>
<Keydown on:combo={handleKeypress} /> <svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} />
<svelte:window on:copyImage={async () => await doCopy()} />
<div <div
transition:fade={{ duration: 150 }} transition:fade={{ duration: 150 }}
class="flex place-items-center place-content-center h-full select-none" class="flex place-items-center place-content-center h-full select-none"
> >
{#if assetInfo}
{#await loadAssetData()} {#await loadAssetData()}
<LoadingSpinner /> <LoadingSpinner />
{:then assetData} {:then assetData}
<img <img
transition:fade={{ duration: 150 }} transition:fade={{ duration: 150 }}
src={assetData} src={assetData}
alt={assetId} alt={asset.id}
class="object-contain h-full transition-all" class="object-contain h-full transition-all"
loading="lazy"
draggable="false" draggable="false"
/> />
{/await} {/await}
{/if}
</div> </div>

View File

@ -1,40 +1,17 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { api, AssetResponseDto, getFileUrl } from '@api'; import { getFileUrl } from '@api';
export let assetId: string; export let assetId: string;
export let publicSharedKey = ''; export let publicSharedKey = '';
let asset: AssetResponseDto;
let isVideoLoading = true; let isVideoLoading = true;
let videoUrl: string;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
onMount(async () => { const handleCanPlay = (ev: Event & { currentTarget: HTMLVideoElement }) => {
const { data: assetInfo } = await api.assetApi.getAssetById(assetId, { const playerNode = ev.currentTarget;
params: {
key: publicSharedKey
}
});
await loadVideoData(assetInfo);
asset = assetInfo;
});
const loadVideoData = async (assetInfo: AssetResponseDto) => {
isVideoLoading = true;
videoUrl = getFileUrl(assetInfo.id, false, true, publicSharedKey);
return assetInfo;
};
const handleCanPlay = (ev: Event) => {
const playerNode = ev.target as HTMLVideoElement;
playerNode.muted = true; playerNode.muted = true;
playerNode.play(); playerNode.play();
@ -48,14 +25,13 @@
transition:fade={{ duration: 150 }} transition:fade={{ duration: 150 }}
class="flex place-items-center place-content-center h-full select-none" class="flex place-items-center place-content-center h-full select-none"
> >
{#if asset}
<video <video
controls controls
class="h-full object-contain" class="h-full object-contain"
src={getFileUrl(assetId, false, true, publicSharedKey)}
on:canplay={handleCanPlay} on:canplay={handleCanPlay}
on:ended={() => dispatch('onVideoEnded')} on:ended={() => dispatch('onVideoEnded')}
> >
<source src={videoUrl} type="video/mp4" />
<track kind="captions" /> <track kind="captions" />
</video> </video>
@ -64,5 +40,4 @@
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
{/if} {/if}
{/if}
</div> </div>

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { api } from '@api'; import { api } from '@api';
import ImmichLogo from '../shared-components/immich-logo.svelte';
let error: string; let error: string;
let success: string; let success: string;
@ -55,14 +56,7 @@
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg" class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg"
> >
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<img <ImmichLogo class="text-center" height="100" width="100" />
class="text-center"
src="/immich-logo.svg"
height="100"
width="100"
alt="immich-logo"
draggable="false"
/>
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium"> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
Admin Registration Admin Registration
</h1> </h1>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { api, UserResponseDto } from '@api'; import { api, UserResponseDto } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
let error: string; let error: string;
@ -47,14 +48,7 @@
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg" class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg"
> >
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<img <ImmichLogo class="text-center" height="100" width="100" />
class="text-center"
src="/immich-logo.svg"
height="100"
width="100"
alt="immich-logo"
draggable="false"
/>
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium"> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
Change Password Change Password
</h1> </h1>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { api } from '@api'; import { api } from '@api';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
import { import {
notificationController, notificationController,
NotificationType NotificationType
@ -80,14 +81,7 @@
class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg" class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg"
> >
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<img <ImmichLogo class="text-center" height="100" width="100" />
class="text-center"
src="/immich-logo.svg"
height="100"
width="100"
alt="immich-logo"
draggable="false"
/>
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium"> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">
Create new user Create new user
</h1> </h1>

View File

@ -5,6 +5,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { api, oauth, OAuthConfigResponseDto } from '@api'; import { api, oauth, OAuthConfigResponseDto } from '@api';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
let error: string; let error: string;
let email = ''; let email = '';
@ -77,14 +78,7 @@
class="border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-md py-8" class="border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-md py-8"
> >
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<img <ImmichLogo class="text-center" height="100" width="100" />
class="text-center"
src="/immich-logo.svg"
height="100"
width="100"
alt="immich-logo"
draggable="false"
/>
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Login</h1> <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Login</h1>
</div> </div>

View File

@ -17,6 +17,7 @@
notificationController, notificationController,
NotificationType NotificationType
} from '../shared-components/notification/notification'; } from '../shared-components/notification/notification';
import ImmichLogo from '../shared-components/immich-logo.svelte';
export let sharedLink: SharedLinkResponseDto; export let sharedLink: SharedLinkResponseDto;
export let isOwned: boolean; export let isOwned: boolean;
@ -122,7 +123,7 @@
class="flex gap-2 place-items-center hover:cursor-pointer ml-6" class="flex gap-2 place-items-center hover:cursor-pointer ml-6"
href="https://immich.app" href="https://immich.app"
> >
<img src="/immich-logo.svg" alt="immich logo" height="30" width="30" draggable="false" /> <ImmichLogo height="30" width="30" />
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary"> <h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">
IMMICH IMMICH
</h1> </h1>

View File

@ -2,31 +2,21 @@
/** /**
* This is the circle icon component. * This is the circle icon component.
*/ */
import { createEventDispatcher } from 'svelte'; import type Icon from 'svelte-material-icons/AbTesting.svelte';
// TODO: why any here? export let logo: typeof Icon;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let logo: any;
export let backgroundColor = 'transparent'; export let backgroundColor = 'transparent';
export let hoverColor = '#e2e7e9'; export let hoverColor = '#e2e7e9';
export let size = '24'; export let size = '24';
export let title = ''; export let title = '';
let iconButton: HTMLButtonElement;
const dispatch = createEventDispatcher();
$: {
if (iconButton) {
iconButton.style.backgroundColor = backgroundColor;
iconButton.style.setProperty('--immich-icon-button-hover-color', hoverColor);
}
}
</script> </script>
<button <button
{title} {title}
bind:this={iconButton} style:backgroundColor
style:--immich-icon-button-hover-color={hoverColor}
class={`immich-circle-icon-button dark:text-immich-dark-fg hover:dark:text-immich-dark-gray rounded-full p-3 flex place-items-center place-content-center transition-all`} class={`immich-circle-icon-button dark:text-immich-dark-fg hover:dark:text-immich-dark-gray rounded-full p-3 flex place-items-center place-content-center transition-all`}
on:click={(mouseEvent) => dispatch('click', { mouseEvent })} on:click
> >
<svelte:component this={logo} {size} /> <svelte:component this={logo} {size} />
</button> </button>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import ImmichLogo from './immich-logo.svelte';
export let dropHandler: (event: DragEvent) => void; export let dropHandler: (event: DragEvent) => void;
export let dragOverHandler: (event: DragEvent) => void; export let dragOverHandler: (event: DragEvent) => void;
@ -14,13 +15,6 @@
on:dragleave={dragLeaveHandler} on:dragleave={dragLeaveHandler}
class="fixed inset-0 w-full h-full z-[1000] flex flex-col items-center justify-center bg-gray-100/90 dark:bg-immich-dark-bg/90 text-immich-dark-gray dark:text-immich-gray" class="fixed inset-0 w-full h-full z-[1000] flex flex-col items-center justify-center bg-gray-100/90 dark:bg-immich-dark-bg/90 text-immich-dark-gray dark:text-immich-gray"
> >
<img <ImmichLogo height="200" width="200" class="animate-bounce pb-16" />
src="/immich-logo.svg"
alt="immich logo"
height="200"
width="200"
class="animate-bounce pb-16"
draggable="false"
/>
<div class="text-2xl">Drop files anywhere to upload</div> <div class="text-2xl">Drop files anywhere to upload</div>
</div> </div>

View File

@ -0,0 +1,7 @@
<script lang="ts">
import immichLogoUrl from '$lib/assets/immich-logo.svg';
export let draggable = false;
</script>
<img src={immichLogoUrl} alt="Immich Logo" {draggable} {...$$restProps} />

View File

@ -8,6 +8,7 @@
import ThemeButton from '../theme-button.svelte'; import ThemeButton from '../theme-button.svelte';
import { AppRoute } from '../../../constants'; import { AppRoute } from '../../../constants';
import AccountInfoPanel from './account-info-panel.svelte'; import AccountInfoPanel from './account-info-panel.svelte';
import ImmichLogo from '../immich-logo.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
export let shouldShowUploadButton = true; export let shouldShowUploadButton = true;
@ -50,7 +51,7 @@
class="flex gap-2 place-items-center hover:cursor-pointer" class="flex gap-2 place-items-center hover:cursor-pointer"
href="/photos" href="/photos"
> >
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" draggable="false" /> <ImmichLogo height="35" width="35" />
<h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary"> <h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary">
IMMICH IMMICH
</h1> </h1>

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type Icon from 'svelte-material-icons/AbTesting.svelte';
export let title: string; export let title: string;
// TODO: why `any` here? There should be a expected type for this export let logo: typeof Icon;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let logo: any;
export let isSelected: boolean; export let isSelected: boolean;
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';

View File

@ -2,6 +2,7 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { asByteUnitString } from '$lib/utils/byte-units'; import { asByteUnitString } from '$lib/utils/byte-units';
import { UploadAsset } from '$lib/models/upload-asset'; import { UploadAsset } from '$lib/models/upload-asset';
import ImmichLogo from './immich-logo.svelte';
export let uploadAsset: UploadAsset; export let uploadAsset: UploadAsset;
@ -16,13 +17,9 @@
> >
<div class="relative"> <div class="relative">
{#if showFallbackImage} {#if showFallbackImage}
<img <div in:fade={{ duration: 250 }}>
in:fade={{ duration: 250 }} <ImmichLogo class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg" />
src="immich-logo.svg" </div>
alt="Immich Logo"
class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg"
draggable="false"
/>
{:else} {:else}
<img <img
in:fade={{ duration: 250 }} in:fade={{ duration: 250 }}

View File

@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { AlbumResponseDto, api, ThumbnailFormat, UserResponseDto } from '@api'; import { AlbumResponseDto, api, ThumbnailFormat, UserResponseDto } from '@api';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let user: UserResponseDto; export let user: UserResponseDto;
const loadImageData = async (thubmnailId: string | null) => { const loadImageData = async (thubmnailId: string | null) => {
if (thubmnailId == null) { if (thubmnailId == null) {
return '/no-thumbnail.png'; return noThumbnailUrl;
} }
const { data } = await api.assetApi.getAssetThumbnail(thubmnailId, ThumbnailFormat.Webp, { const { data } = await api.assetApi.getAssetThumbnail(thubmnailId, ThumbnailFormat.Webp, {

View File

@ -9,6 +9,7 @@
NotificationType NotificationType
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
const handleCopy = async () => { const handleCopy = async () => {
// //
@ -33,7 +34,7 @@
<section class="bg-immich-bg dark:bg-immich-dark-bg"> <section class="bg-immich-bg dark:bg-immich-dark-bg">
<div class="flex border-b dark:border-b-immich-dark-gray place-items-center px-6 py-4"> <div class="flex border-b dark:border-b-immich-dark-gray place-items-center px-6 py-4">
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos"> <a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" draggable="false" /> <ImmichLogo height="35" width="35" />
<h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary"> <h1 class="font-immich-title text-2xl text-immich-primary dark:text-immich-dark-primary">
IMMICH IMMICH
</h1> </h1>

View File

@ -13,6 +13,7 @@
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte'; import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte'; import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
import { fileUploadHandler } from '$lib/utils/file-uploader'; import { fileUploadHandler } from '$lib/utils/file-uploader';
import faviconUrl from '$lib/assets/favicon.png';
let shouldShowAnnouncement: boolean; let shouldShowAnnouncement: boolean;
let localVersion: string; let localVersion: string;
@ -80,6 +81,7 @@
<svelte:head> <svelte:head>
<title>{$page.data.meta?.title || 'Web'} - Immich</title> <title>{$page.data.meta?.title || 'Web'} - Immich</title>
{#if $page.data.meta} {#if $page.data.meta}
<link rel="icon" href={faviconUrl} />
<meta name="description" content={$page.data.meta.description} /> <meta name="description" content={$page.data.meta.description} />
<!-- Facebook Meta Tags --> <!-- Facebook Meta Tags -->

View File

@ -1,18 +1,12 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
</script> </script>
<section class="h-screen w-screen flex place-items-center place-content-center"> <section class="h-screen w-screen flex place-items-center place-content-center">
<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]"> <div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
<div class="flex place-items-center place-content-center "> <div class="flex place-items-center place-content-center ">
<img <ImmichLogo class="text-center" height="200" width="200" />
class="text-center"
src="immich-logo.svg"
height="200"
width="200"
alt="immich-logo"
draggable="false"
/>
</div> </div>
<h1 <h1
class="text-4xl text-immich-primary dark:text-immich-dark-primary font-bold font-immich-title" class="text-4xl text-immich-primary dark:text-immich-dark-primary font-bold font-immich-title"

View File

@ -10,6 +10,7 @@
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte'; import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte'; import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
import { useAlbums } from './albums.bloc'; import { useAlbums } from './albums.bloc';
import empty1Url from '$lib/assets/empty-1.svg';
export let data: PageData; export let data: PageData;
@ -93,7 +94,7 @@
on:keydown={handleCreateAlbum} on:keydown={handleCreateAlbum}
class="border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center" class="border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center"
> >
<img src="/empty-1.svg" alt="Empty shared album" width="500" draggable="false" /> <img src={empty1Url} alt="Empty shared album" width="500" draggable="false" />
<p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg"> <p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg">
Create an album to organize your photos and videos Create an album to organize your photos and videos

View File

@ -13,6 +13,7 @@
import StarMinusOutline from 'svelte-material-icons/StarMinusOutline.svelte'; import StarMinusOutline from 'svelte-material-icons/StarMinusOutline.svelte';
import Error from '../+error.svelte'; import Error from '../+error.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import empty1Url from '$lib/assets/empty-1.svg';
export let data: PageData; export let data: PageData;
@ -126,7 +127,7 @@
<div <div
class="border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center" class="border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center"
> >
<img src="/empty-1.svg" alt="Empty shared album" width="500" draggable="false" /> <img src={empty1Url} alt="Empty shared album" width="500" draggable="false" />
<p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg"> <p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg">
Add favorites to quickly find your best pictures and videos Add favorites to quickly find your best pictures and videos

View File

@ -64,12 +64,8 @@
let isShowAlbumPicker = false; let isShowAlbumPicker = false;
let addToSharedAlbum = false; let addToSharedAlbum = false;
const handleShowMenu = (event: CustomEvent) => { const handleShowMenu = ({ x, y }: MouseEvent) => {
contextMenuPosition = { contextMenuPosition = { x, y };
x: event.detail.mouseEvent.x,
y: event.detail.mouseEvent.y
};
isShowAddMenu = !isShowAddMenu; isShowAddMenu = !isShowAddMenu;
}; };

View File

@ -4,6 +4,7 @@ import { error } from '@sveltejs/kit';
import { getThumbnailUrl } from '$lib/utils/asset-utils'; import { getThumbnailUrl } from '$lib/utils/asset-utils';
import { serverApi, ThumbnailFormat } from '@api'; import { serverApi, ThumbnailFormat } from '@api';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import featurePanelUrl from '$lib/assets/feature-panel.png';
export const load: PageServerLoad = async ({ params, parent }) => { export const load: PageServerLoad = async ({ params, parent }) => {
const { user } = await parent(); const { user } = await parent();
@ -23,7 +24,7 @@ export const load: PageServerLoad = async ({ params, parent }) => {
description: sharedLink.description || `${assetCount} shared photos & videos.`, description: sharedLink.description || `${assetCount} shared photos & videos.`,
imageUrl: assetId imageUrl: assetId
? getThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key) ? getThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key)
: 'feature-panel.png' : featurePanelUrl
}, },
user user
}; };

View File

@ -12,6 +12,7 @@
notificationController, notificationController,
NotificationType NotificationType
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import empty2Url from '$lib/assets/empty-2.svg';
export let data: PageData; export let data: PageData;
@ -94,7 +95,7 @@
<div <div
class="border dark:border-immich-dark-gray p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center dark:text-immich-dark-fg" class="border dark:border-immich-dark-gray p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center dark:text-immich-dark-fg"
> >
<img src="/empty-2.svg" alt="Empty shared album" width="500" draggable="false" /> <img src={empty2Url} alt="Empty shared album" width="500" draggable="false" />
<p class="text-center text-immich-text-gray-500"> <p class="text-center text-immich-text-gray-500">
Create a shared album to share photos and videos with people in your network Create a shared album to share photos and videos with people in your network
</p> </p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB