mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 06:31:58 +00:00
feat(mobile): add additional request headers (#10588)
* add additional request headers * improve interface * move headers under advanced settings * refactor * refactor --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
a3c3619811
commit
922430da36
23 changed files with 319 additions and 47 deletions
|
@ -13,6 +13,9 @@
|
||||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
|
||||||
"advanced_settings_tile_subtitle": "Advanced user's settings",
|
"advanced_settings_tile_subtitle": "Advanced user's settings",
|
||||||
"advanced_settings_tile_title": "Advanced",
|
"advanced_settings_tile_title": "Advanced",
|
||||||
|
"headers_settings_tile_title": "Custom proxy headers",
|
||||||
|
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
|
||||||
|
|
||||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||||
"album_info_card_backup_album_excluded": "EXCLUDED",
|
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||||
|
@ -522,5 +525,8 @@
|
||||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||||
"viewer_remove_from_stack": "Remove from Stack",
|
"viewer_remove_from_stack": "Remove from Stack",
|
||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
"viewer_unstack": "Un-Stack"
|
"viewer_unstack": "Un-Stack",
|
||||||
}
|
"header_settings_header_name_input": "Header name",
|
||||||
|
"header_settings_header_value_input": "Header value",
|
||||||
|
"header_settings_page_title": "Proxy Headers"
|
||||||
|
}
|
||||||
|
|
|
@ -193,6 +193,7 @@ enum StoreKey<T> {
|
||||||
mapThemeMode<int>(124, type: int),
|
mapThemeMode<int>(124, type: int),
|
||||||
mapwithPartners<bool>(125, type: bool),
|
mapwithPartners<bool>(125, type: bool),
|
||||||
enableHapticFeedback<bool>(126, type: bool),
|
enableHapticFeedback<bool>(126, type: bool),
|
||||||
|
customHeaders<String>(127, type: String),
|
||||||
;
|
;
|
||||||
|
|
||||||
const StoreKey(
|
const StoreKey(
|
||||||
|
|
183
mobile/lib/pages/common/headers_settings.page.dart
Normal file
183
mobile/lib/pages/common/headers_settings.page.dart
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart' as store_keys;
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class SettingsHeader {
|
||||||
|
String key = "";
|
||||||
|
String value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class HeaderSettingsPage extends HookConsumerWidget {
|
||||||
|
const HeaderSettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// final apiService = ref.watch(apiServiceProvider);
|
||||||
|
final headers = useState<List<SettingsHeader>>([]);
|
||||||
|
final setInitialHeaders = useState(false);
|
||||||
|
|
||||||
|
var headersStr =
|
||||||
|
store_keys.Store.get(store_keys.StoreKey.customHeaders, "");
|
||||||
|
if (!setInitialHeaders.value) {
|
||||||
|
if (headersStr.isNotEmpty) {
|
||||||
|
var customHeaders = jsonDecode(headersStr) as Map;
|
||||||
|
customHeaders.forEach((k, v) {
|
||||||
|
final header = SettingsHeader();
|
||||||
|
header.key = k;
|
||||||
|
header.value = v;
|
||||||
|
headers.value.add(header);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// add first one to help the user
|
||||||
|
if (headers.value.isEmpty) {
|
||||||
|
final header = SettingsHeader();
|
||||||
|
header.key = '';
|
||||||
|
header.value = '';
|
||||||
|
|
||||||
|
headers.value.add(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInitialHeaders.value = true;
|
||||||
|
|
||||||
|
var list = [
|
||||||
|
...headers.value.map((headerValue) {
|
||||||
|
return HeaderKeyValueSettings(
|
||||||
|
header: headerValue,
|
||||||
|
onRemove: () {
|
||||||
|
headers.value.remove(headerValue);
|
||||||
|
headers.value = headers.value.toList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('header_settings_page_title').tr(),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
headers.value.add(SettingsHeader());
|
||||||
|
headers.value = headers.value.toList();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add_outlined),
|
||||||
|
tooltip: 'Add Header',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: PopScope(
|
||||||
|
onPopInvoked: (_) => saveHeaders(headers.value),
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
|
||||||
|
itemCount: list.length,
|
||||||
|
itemBuilder: (ctx, index) => list[index],
|
||||||
|
separatorBuilder: (context, index) => const Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 16.0, left: 8, right: 8),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveHeaders(List<SettingsHeader> headers) {
|
||||||
|
final headersMap = {};
|
||||||
|
for (var header in headers) {
|
||||||
|
final key = header.key.trim();
|
||||||
|
final value = header.value.trim();
|
||||||
|
|
||||||
|
if (key.isEmpty || value.isEmpty) continue;
|
||||||
|
headersMap[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded = jsonEncode(headersMap);
|
||||||
|
store_keys.Store.put(store_keys.StoreKey.customHeaders, encoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderKeyValueSettings extends StatelessWidget {
|
||||||
|
final TextEditingController keyController;
|
||||||
|
final TextEditingController valueController;
|
||||||
|
final SettingsHeader header;
|
||||||
|
final Function() onRemove;
|
||||||
|
|
||||||
|
HeaderKeyValueSettings({
|
||||||
|
super.key,
|
||||||
|
required this.header,
|
||||||
|
required this.onRemove,
|
||||||
|
}) : keyController = TextEditingController(text: header.key),
|
||||||
|
valueController = TextEditingController(text: header.value);
|
||||||
|
|
||||||
|
String? emptyFieldValidator(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Value cannot be empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: keyController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'header_settings_header_name_input'.tr(),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (headerKey) {
|
||||||
|
header.key = headerKey;
|
||||||
|
},
|
||||||
|
validator: emptyFieldValidator,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: IconButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
color: Colors.red[400],
|
||||||
|
onPressed: onRemove,
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: valueController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'header_settings_header_name_input'.tr(),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
autocorrect: false,
|
||||||
|
onChanged: (headerValue) {
|
||||||
|
header.value = headerValue;
|
||||||
|
},
|
||||||
|
validator: emptyFieldValidator,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
import 'package:immich_mobile/widgets/search/person_name_edit_form.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
|
@ -122,9 +122,7 @@ class PersonResultPage extends HookConsumerWidget {
|
||||||
radius: 36,
|
radius: 36,
|
||||||
backgroundImage: NetworkImage(
|
backgroundImage: NetworkImage(
|
||||||
getFaceThumbnailUrl(personId),
|
getFaceThumbnailUrl(personId),
|
||||||
headers: {
|
headers: ApiService.getRequestHeaders(),
|
||||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
@ -26,11 +27,9 @@ Future<VideoPlayerController> videoPlayerController(
|
||||||
: '$serverEndpoint/assets/${asset.remoteId}/video/playback';
|
: '$serverEndpoint/assets/${asset.remoteId}/video/playback';
|
||||||
|
|
||||||
final url = Uri.parse(videoUrl);
|
final url = Uri.parse(videoUrl);
|
||||||
final accessToken = Store.get(StoreKey.accessToken);
|
|
||||||
|
|
||||||
controller = VideoPlayerController.networkUrl(
|
controller = VideoPlayerController.networkUrl(
|
||||||
url,
|
url,
|
||||||
httpHeaders: {"x-immich-user-token": accessToken},
|
httpHeaders: ApiService.getRequestHeaders(),
|
||||||
videoPlayerOptions: asset.livePhotoVideoId != null
|
videoPlayerOptions: asset.livePhotoVideoId != null
|
||||||
? VideoPlayerOptions(mixWithOthers: true)
|
? VideoPlayerOptions(mixWithOthers: true)
|
||||||
: VideoPlayerOptions(mixWithOthers: false),
|
: VideoPlayerOptions(mixWithOthers: false),
|
||||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@ import 'dart:ui' as ui;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
|
||||||
/// Loads the codec from the URI and sends the events to the [chunkEvents] stream
|
/// Loads the codec from the URI and sends the events to the [chunkEvents] stream
|
||||||
///
|
///
|
||||||
|
@ -17,9 +17,7 @@ class ImageLoader {
|
||||||
required ImageDecoderCallback decode,
|
required ImageDecoderCallback decode,
|
||||||
StreamController<ImageChunkEvent>? chunkEvents,
|
StreamController<ImageChunkEvent>? chunkEvents,
|
||||||
}) async {
|
}) async {
|
||||||
final headers = {
|
final headers = ApiService.getRequestHeaders();
|
||||||
'x-immich-user-token': Store.get(StoreKey.accessToken),
|
|
||||||
};
|
|
||||||
|
|
||||||
final stream = cache.getFileStream(
|
final stream = cache.getFileStream(
|
||||||
uri,
|
uri,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -105,10 +106,9 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
final authenticationState = _ref.read(authenticationProvider);
|
final authenticationState = _ref.read(authenticationProvider);
|
||||||
|
|
||||||
if (authenticationState.isAuthenticated) {
|
if (authenticationState.isAuthenticated) {
|
||||||
final accessToken = Store.get(StoreKey.accessToken);
|
|
||||||
try {
|
try {
|
||||||
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
|
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
|
||||||
final headers = {"x-immich-user-token": accessToken};
|
final headers = ApiService.getRequestHeaders();
|
||||||
if (endpoint.userInfo.isNotEmpty) {
|
if (endpoint.userInfo.isNotEmpty) {
|
||||||
headers["Authorization"] =
|
headers["Authorization"] =
|
||||||
"Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
|
"Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
|
||||||
|
|
|
@ -24,6 +24,7 @@ import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
|
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/headers_settings.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||||
|
@ -222,6 +223,10 @@ class AppRouter extends _$AppRouter {
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
transitionsBuilder: TransitionsBuilders.noTransition,
|
transitionsBuilder: TransitionsBuilders.noTransition,
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: HeaderSettingsRoute.page,
|
||||||
|
guards: [_duplicateGuard],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -191,6 +191,12 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
HeaderSettingsRoute.name: (routeData) {
|
||||||
|
return AutoRoutePage<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: const HeaderSettingsPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
LibraryRoute.name: (routeData) {
|
LibraryRoute.name: (routeData) {
|
||||||
return AutoRoutePage<dynamic>(
|
return AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
|
@ -917,6 +923,20 @@ class GalleryViewerRouteArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [HeaderSettingsPage]
|
||||||
|
class HeaderSettingsRoute extends PageRouteInfo<void> {
|
||||||
|
const HeaderSettingsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
HeaderSettingsRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'HeaderSettingsRoute';
|
||||||
|
|
||||||
|
static const PageInfo<void> page = PageInfo<void>(name);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [LibraryPage]
|
/// [LibraryPage]
|
||||||
class LibraryRoute extends PageRouteInfo<void> {
|
class LibraryRoute extends PageRouteInfo<void> {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
class ApiService {
|
class ApiService implements Authentication {
|
||||||
late ApiClient _apiClient;
|
late ApiClient _apiClient;
|
||||||
|
|
||||||
late UsersApi usersApi;
|
late UsersApi usersApi;
|
||||||
|
@ -40,7 +40,7 @@ class ApiService {
|
||||||
final _log = Logger("ApiService");
|
final _log = Logger("ApiService");
|
||||||
|
|
||||||
setEndpoint(String endpoint) {
|
setEndpoint(String endpoint) {
|
||||||
_apiClient = ApiClient(basePath: endpoint);
|
_apiClient = ApiClient(basePath: endpoint, authentication: this);
|
||||||
if (_accessToken != null) {
|
if (_accessToken != null) {
|
||||||
setAccessToken(_accessToken!);
|
setAccessToken(_accessToken!);
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,10 @@ class ApiService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await client
|
final response = await client
|
||||||
.get(Uri.parse("$serverUrl/server-info/ping"))
|
.get(
|
||||||
|
Uri.parse("$serverUrl/server-info/ping"),
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
)
|
||||||
.timeout(const Duration(seconds: 5));
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
_log.info("Pinging server with response code ${response.statusCode}");
|
_log.info("Pinging server with response code ${response.statusCode}");
|
||||||
|
@ -132,9 +135,12 @@ class ApiService {
|
||||||
final Client client = Client();
|
final Client client = Client();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
var headers = {"Accept": "application/json"};
|
||||||
|
headers.addAll(getRequestHeaders());
|
||||||
|
|
||||||
final res = await client.get(
|
final res = await client.get(
|
||||||
Uri.parse("$baseUrl/.well-known/immich"),
|
Uri.parse("$baseUrl/.well-known/immich"),
|
||||||
headers: {"Accept": "application/json"},
|
headers: headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
|
@ -156,7 +162,38 @@ class ApiService {
|
||||||
|
|
||||||
setAccessToken(String accessToken) {
|
setAccessToken(String accessToken) {
|
||||||
_accessToken = accessToken;
|
_accessToken = accessToken;
|
||||||
_apiClient.addDefaultHeader('x-immich-user-token', accessToken);
|
Store.put(StoreKey.accessToken, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, String> getRequestHeaders() {
|
||||||
|
var accessToken = Store.get(StoreKey.accessToken, "");
|
||||||
|
var customHeadersStr = Store.get(StoreKey.customHeaders, "");
|
||||||
|
var header = <String, String>{};
|
||||||
|
if (accessToken.isNotEmpty) {
|
||||||
|
header['x-immich-user-token'] = accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customHeadersStr.isEmpty) {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
var customHeaders = jsonDecode(customHeadersStr) as Map;
|
||||||
|
customHeaders.forEach((key, value) {
|
||||||
|
header[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> applyToParams(
|
||||||
|
List<QueryParam> queryParams,
|
||||||
|
Map<String, String> headerParams,
|
||||||
|
) {
|
||||||
|
return Future<void>(() {
|
||||||
|
var headers = ApiService.getRequestHeaders();
|
||||||
|
headerParams.addAll(headers);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient get apiClient => _apiClient;
|
ApiClient get apiClient => _apiClient;
|
||||||
|
|
|
@ -303,8 +303,7 @@ class BackupService {
|
||||||
onProgress: ((bytes, totalBytes) =>
|
onProgress: ((bytes, totalBytes) =>
|
||||||
uploadProgressCb(bytes, totalBytes)),
|
uploadProgressCb(bytes, totalBytes)),
|
||||||
);
|
);
|
||||||
baseRequest.headers["x-immich-user-token"] =
|
baseRequest.headers.addAll(ApiService.getRequestHeaders());
|
||||||
Store.get(StoreKey.accessToken);
|
|
||||||
baseRequest.headers["Transfer-Encoding"] = "chunked";
|
baseRequest.headers["Transfer-Encoding"] = "chunked";
|
||||||
|
|
||||||
baseRequest.fields['deviceAssetId'] = entity.id;
|
baseRequest.fields['deviceAssetId'] = entity.id;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
@ -48,9 +48,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
||||||
album,
|
album,
|
||||||
type: AssetMediaSize.thumbnail,
|
type: AssetMediaSize.thumbnail,
|
||||||
),
|
),
|
||||||
httpHeaders: {
|
httpHeaders: ApiService.getRequestHeaders(),
|
||||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
|
||||||
},
|
|
||||||
cacheKey:
|
cacheKey:
|
||||||
getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail),
|
getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail),
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
|
||||||
Widget userAvatar(BuildContext context, User u, {double? radius}) {
|
Widget userAvatar(BuildContext context, User u, {double? radius}) {
|
||||||
final url =
|
final url =
|
||||||
|
@ -13,7 +14,7 @@ Widget userAvatar(BuildContext context, User u, {double? radius}) {
|
||||||
backgroundColor: context.primaryColor.withAlpha(50),
|
backgroundColor: context.primaryColor.withAlpha(50),
|
||||||
foregroundImage: CachedNetworkImageProvider(
|
foregroundImage: CachedNetworkImageProvider(
|
||||||
url,
|
url,
|
||||||
headers: {"x-immich-user-token": Store.get(StoreKey.accessToken)},
|
headers: ApiService.getRequestHeaders(),
|
||||||
cacheKey: "user-${u.id}-profile",
|
cacheKey: "user-${u.id}-profile",
|
||||||
),
|
),
|
||||||
// silence errors if user has no profile image, use initials as fallback
|
// silence errors if user has no profile image, use initials as fallback
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/widgets/common/transparent_image.dart';
|
import 'package:immich_mobile/widgets/common/transparent_image.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
|
@ -50,9 +51,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||||
height: size,
|
height: size,
|
||||||
placeholder: (_, __) => Image.memory(kTransparentImage),
|
placeholder: (_, __) => Image.memory(kTransparentImage),
|
||||||
imageUrl: profileImageUrl,
|
imageUrl: profileImageUrl,
|
||||||
httpHeaders: {
|
httpHeaders: ApiService.getRequestHeaders(),
|
||||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
|
||||||
},
|
|
||||||
fadeInDuration: const Duration(milliseconds: 300),
|
fadeInDuration: const Duration(milliseconds: 300),
|
||||||
errorWidget: (context, error, stackTrace) => textIcon,
|
errorWidget: (context, error, stackTrace) => textIcon,
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'dart:math';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
class PositionedAssetMarkerIcon extends StatelessWidget {
|
class PositionedAssetMarkerIcon extends StatelessWidget {
|
||||||
|
@ -88,9 +88,7 @@ class _AssetMarkerIcon extends StatelessWidget {
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
imageUrl,
|
imageUrl,
|
||||||
cacheKey: cacheKey,
|
cacheKey: cacheKey,
|
||||||
headers: {
|
headers: ApiService.getRequestHeaders(),
|
||||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
|
||||||
},
|
|
||||||
errorListener: (_) =>
|
errorListener: (_) =>
|
||||||
const Icon(Icons.image_not_supported_outlined),
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
import 'package:immich_mobile/models/search/search_curated_content.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
class CuratedPeopleRow extends StatelessWidget {
|
class CuratedPeopleRow extends StatelessWidget {
|
||||||
|
@ -33,9 +33,7 @@ class CuratedPeopleRow extends StatelessWidget {
|
||||||
separatorBuilder: (context, index) => const SizedBox(width: 16),
|
separatorBuilder: (context, index) => const SizedBox(width: 16),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final person = content[index];
|
final person = content[index];
|
||||||
final headers = {
|
final headers = ApiService.getRequestHeaders();
|
||||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
|
||||||
};
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart' as local_store;
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
@ -18,10 +18,7 @@ class PeoplePicker extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var imageSize = 45.0;
|
var imageSize = 45.0;
|
||||||
final people = ref.watch(getAllPeopleProvider);
|
final people = ref.watch(getAllPeopleProvider);
|
||||||
final headers = {
|
final headers = ApiService.getRequestHeaders();
|
||||||
"x-immich-user-token":
|
|
||||||
local_store.Store.get(local_store.StoreKey.accessToken),
|
|
||||||
};
|
|
||||||
final selectedPeople = useState<Set<PersonResponseDto>>(filter ?? {});
|
final selectedPeople = useState<Set<PersonResponseDto>>(filter ?? {});
|
||||||
|
|
||||||
return people.widgetWhen(
|
return people.widgetWhen(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart';
|
import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart';
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
|
||||||
class ThumbnailWithInfo extends StatelessWidget {
|
class ThumbnailWithInfo extends StatelessWidget {
|
||||||
const ThumbnailWithInfo({
|
const ThumbnailWithInfo({
|
||||||
|
@ -36,9 +36,7 @@ class ThumbnailWithInfo extends StatelessWidget {
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: imageUrl!,
|
imageUrl: imageUrl!,
|
||||||
httpHeaders: {
|
httpHeaders: ApiService.getRequestHeaders(),
|
||||||
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
|
||||||
},
|
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
const Icon(Icons.image_not_supported_outlined),
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
|
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
|
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
|
@ -62,6 +63,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||||
subtitle: "advanced_settings_self_signed_ssl_subtitle".tr(),
|
subtitle: "advanced_settings_self_signed_ssl_subtitle".tr(),
|
||||||
onChanged: (_) => HttpOverrides.global = HttpSSLCertOverride(),
|
onChanged: (_) => HttpOverrides.global = HttpSSLCertOverride(),
|
||||||
),
|
),
|
||||||
|
const CustomeProxyHeaderSettings(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return SettingsSubPageScaffold(settings: advancedSettings);
|
return SettingsSubPageScaffold(settings: advancedSettings);
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class CustomeProxyHeaderSettings extends StatelessWidget {
|
||||||
|
const CustomeProxyHeaderSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
"headers_settings_tile_title".tr(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"headers_settings_tile_subtitle".tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => context.pushRoute(const HeaderSettingsRoute()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,9 @@ class LocalStorageSettings extends HookConsumerWidget {
|
||||||
).tr(args: ["${cacheItemCount.value}"]),
|
).tr(args: ["${cacheItemCount.value}"]),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
"cache_settings_duplicated_assets_subtitle",
|
"cache_settings_duplicated_assets_subtitle",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
trailing: TextButton(
|
trailing: TextButton(
|
||||||
onPressed: cacheItemCount.value > 0 ? clearCache : null,
|
onPressed: cacheItemCount.value > 0 ? clearCache : null,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
.PHONY: build watch create_app_icon create_splash build_release_android
|
||||||
|
|
||||||
build:
|
build:
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue