refactored HTTP error handling and streamlined UI components by centralizing shared logic and removing redundant parameters
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
class ApiRequest {}
|
|
||||||
@@ -6,6 +6,7 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:nextcloud/nextcloud.dart';
|
import 'package:nextcloud/nextcloud.dart';
|
||||||
|
|
||||||
import '../api_error.dart';
|
import '../api_error.dart';
|
||||||
|
import '../http_errors.dart';
|
||||||
import '../marianumcloud/talk/talk_error.dart';
|
import '../marianumcloud/talk/talk_error.dart';
|
||||||
import 'app_exception.dart';
|
import 'app_exception.dart';
|
||||||
import 'auth_exception.dart';
|
import 'auth_exception.dart';
|
||||||
@@ -59,9 +60,8 @@ AppException? _dioToAppException(DioException error) {
|
|||||||
/// status plus a trimmed body preview (same format as the Talk API errors).
|
/// status plus a trimmed body preview (same format as the Talk API errors).
|
||||||
AppException _dynamiteToAppException(DynamiteApiException error) {
|
AppException _dynamiteToAppException(DynamiteApiException error) {
|
||||||
final status = error.statusCode;
|
final status = error.statusCode;
|
||||||
final body = error.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
final preview = previewBody(error.body);
|
||||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
final detail = preview.isEmpty ? 'HTTP $status' : 'HTTP $status body=$preview';
|
||||||
final detail = body.isEmpty ? 'HTTP $status' : 'HTTP $status body=$preview';
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
return AuthException.unauthorized(technicalDetails: detail);
|
return AuthException.unauthorized(technicalDetails: detail);
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import 'errors/auth_exception.dart';
|
||||||
|
import 'errors/network_exception.dart';
|
||||||
|
import 'errors/not_found_exception.dart';
|
||||||
|
import 'errors/server_exception.dart';
|
||||||
|
|
||||||
|
/// Runs [send] and converts transport-level failures (socket/timeout/client
|
||||||
|
/// errors) into a [NetworkException] tagged with [label] (e.g. `Talk <uri>`).
|
||||||
|
/// Passes through whatever [send] produces, including `null` for the base-class
|
||||||
|
/// request hooks that may skip the call.
|
||||||
|
Future<http.Response?> sendGuarded(
|
||||||
|
String label,
|
||||||
|
Future<http.Response>? Function() send,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await send();
|
||||||
|
} on SocketException catch (e) {
|
||||||
|
throw NetworkException(technicalDetails: '$label: ${e.message}');
|
||||||
|
} on TimeoutException catch (e) {
|
||||||
|
throw NetworkException.timeout(technicalDetails: '$label: $e');
|
||||||
|
} on http.ClientException catch (e) {
|
||||||
|
throw NetworkException(technicalDetails: '$label: ${e.message}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collapses whitespace and caps an HTTP error body at 500 chars so it can be
|
||||||
|
/// embedded in an [AppException]'s technical details without dumping headers.
|
||||||
|
String previewBody(String body) {
|
||||||
|
final collapsed = body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||||
|
return collapsed.length > 500 ? '${collapsed.substring(0, 500)}…' : collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a `<label> -> HTTP <status>[ body=<preview>]` technical detail line.
|
||||||
|
String httpErrorDetail(String label, String body, int status) {
|
||||||
|
final preview = previewBody(body);
|
||||||
|
return preview.isEmpty
|
||||||
|
? '$label -> HTTP $status'
|
||||||
|
: '$label -> HTTP $status body=$preview';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Throws the [AppException] matching a non-2xx HTTP [status], carrying
|
||||||
|
/// [detail] as technical details: 401/403 map to auth errors, 404 to
|
||||||
|
/// not-found, everything else to a generic server error.
|
||||||
|
Never throwForStatus(int status, String detail) {
|
||||||
|
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail);
|
||||||
|
if (status == 403) throw AuthException.forbidden(technicalDetails: detail);
|
||||||
|
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
||||||
|
throw ServerException(statusCode: status, technicalDetails: detail);
|
||||||
|
}
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../../../model/account_data.dart';
|
import '../../../model/account_data.dart';
|
||||||
import '../../../model/endpoint_data.dart';
|
import '../../../model/endpoint_data.dart';
|
||||||
import '../../errors/auth_exception.dart';
|
|
||||||
import '../../errors/network_exception.dart';
|
|
||||||
import '../../errors/not_found_exception.dart';
|
|
||||||
import '../../errors/parse_exception.dart';
|
import '../../errors/parse_exception.dart';
|
||||||
import '../../errors/server_exception.dart';
|
import '../../http_errors.dart';
|
||||||
import '../nextcloud_ocs.dart';
|
import '../nextcloud_ocs.dart';
|
||||||
|
|
||||||
/// Mix of two Nextcloud surfaces:
|
/// Mix of two Nextcloud surfaces:
|
||||||
@@ -42,30 +37,17 @@ Future<http.Response> _send(
|
|||||||
) async {
|
) async {
|
||||||
final headers = NextcloudOcs.headers();
|
final headers = NextcloudOcs.headers();
|
||||||
|
|
||||||
final http.Response response;
|
final response = (await sendGuarded(
|
||||||
try {
|
'Cloud $uri',
|
||||||
response = await perform(uri, headers);
|
() => perform(uri, headers),
|
||||||
} on SocketException catch (e) {
|
))!;
|
||||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
|
||||||
} on TimeoutException catch (e) {
|
|
||||||
throw NetworkException.timeout(technicalDetails: 'Cloud $uri: $e');
|
|
||||||
} on http.ClientException catch (e) {
|
|
||||||
throw NetworkException(technicalDetails: 'Cloud $uri: ${e.message}');
|
|
||||||
}
|
|
||||||
|
|
||||||
final status = response.statusCode;
|
final status = response.statusCode;
|
||||||
if (status >= 200 && status < 300) return response;
|
if (status >= 200 && status < 300) return response;
|
||||||
|
|
||||||
final body = response.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
final detail = httpErrorDetail('Cloud $uri', response.body, status);
|
||||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
|
||||||
final detail = body.isEmpty
|
|
||||||
? 'Cloud $uri -> HTTP $status'
|
|
||||||
: 'Cloud $uri -> HTTP $status body=$preview';
|
|
||||||
log(detail);
|
log(detail);
|
||||||
if (status == 401) throw AuthException.unauthorized(technicalDetails: detail);
|
throwForStatus(status, detail);
|
||||||
if (status == 403) throw AuthException.forbidden(technicalDetails: detail);
|
|
||||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
|
||||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SetUserAvatar {
|
class SetUserAvatar {
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../../../errors/network_exception.dart';
|
|
||||||
import '../../../errors/server_exception.dart';
|
import '../../../errors/server_exception.dart';
|
||||||
|
import '../../../http_errors.dart';
|
||||||
import '../../nextcloud_ocs.dart';
|
import '../../nextcloud_ocs.dart';
|
||||||
import 'get_chat_params.dart';
|
import 'get_chat_params.dart';
|
||||||
import 'get_chat_response.dart';
|
import 'get_chat_response.dart';
|
||||||
@@ -40,18 +37,12 @@ class LongPollChat {
|
|||||||
);
|
);
|
||||||
final headers = NextcloudOcs.headers();
|
final headers = NextcloudOcs.headers();
|
||||||
|
|
||||||
final http.Response response;
|
final response = (await sendGuarded(
|
||||||
try {
|
'LongPollChat $uri',
|
||||||
response = await http
|
() => http
|
||||||
.get(uri, headers: headers)
|
.get(uri, headers: headers)
|
||||||
.timeout(Duration(seconds: timeoutSeconds + 15));
|
.timeout(Duration(seconds: timeoutSeconds + 15)),
|
||||||
} on TimeoutException catch (e) {
|
))!;
|
||||||
throw NetworkException.timeout(technicalDetails: 'LongPollChat $uri: $e');
|
|
||||||
} on SocketException catch (e) {
|
|
||||||
throw NetworkException(technicalDetails: 'LongPollChat $uri: ${e.message}');
|
|
||||||
} on http.ClientException catch (e) {
|
|
||||||
throw NetworkException(technicalDetails: 'LongPollChat $uri: ${e.message}');
|
|
||||||
}
|
|
||||||
|
|
||||||
final status = response.statusCode;
|
final status = response.statusCode;
|
||||||
if (status == 304) return null;
|
if (status == 304) return null;
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../../api_params.dart';
|
import '../../api_params.dart';
|
||||||
import '../../api_request.dart';
|
|
||||||
import '../../api_response.dart';
|
import '../../api_response.dart';
|
||||||
import '../../errors/auth_exception.dart';
|
|
||||||
import '../../errors/network_exception.dart';
|
import '../../errors/network_exception.dart';
|
||||||
import '../../errors/not_found_exception.dart';
|
|
||||||
import '../../errors/parse_exception.dart';
|
import '../../errors/parse_exception.dart';
|
||||||
import '../../errors/server_exception.dart';
|
import '../../http_errors.dart';
|
||||||
import '../nextcloud_ocs.dart';
|
import '../nextcloud_ocs.dart';
|
||||||
|
|
||||||
enum TalkApiMethod { get, post, put, delete }
|
abstract class TalkApi<T extends ApiResponse?> {
|
||||||
|
|
||||||
abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
|
||||||
String path;
|
String path;
|
||||||
ApiParams? body;
|
ApiParams? body;
|
||||||
Map<String, String>? headers;
|
Map<String, String>? headers;
|
||||||
Map<String, dynamic>? getParameters;
|
Map<String, dynamic>? getParameters;
|
||||||
|
|
||||||
http.Response? response;
|
|
||||||
|
|
||||||
TalkApi(this.path, this.body, {this.headers, this.getParameters});
|
TalkApi(this.path, this.body, {this.headers, this.getParameters});
|
||||||
|
|
||||||
Future<http.Response>? request(
|
Future<http.Response>? request(
|
||||||
@@ -40,43 +31,25 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
|||||||
);
|
);
|
||||||
final mergedHeaders = {...NextcloudOcs.headers(), ...?headers};
|
final mergedHeaders = {...NextcloudOcs.headers(), ...?headers};
|
||||||
|
|
||||||
final http.Response data;
|
final data = await sendGuarded(
|
||||||
try {
|
'Talk $endpoint',
|
||||||
final raw = await request(endpoint, body, mergedHeaders);
|
() => request(endpoint, body, mergedHeaders),
|
||||||
if (raw == null) {
|
);
|
||||||
|
if (data == null) {
|
||||||
throw const NetworkException(
|
throw const NetworkException(
|
||||||
userMessage: 'Keine Antwort vom Talk-Server erhalten.',
|
userMessage: 'Keine Antwort vom Talk-Server erhalten.',
|
||||||
technicalDetails: 'Talk request returned null',
|
technicalDetails: 'Talk request returned null',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
data = raw;
|
|
||||||
} on SocketException catch (e) {
|
|
||||||
throw NetworkException(technicalDetails: 'Talk $endpoint: ${e.message}');
|
|
||||||
} on TimeoutException catch (e) {
|
|
||||||
throw NetworkException.timeout(technicalDetails: 'Talk $endpoint: $e');
|
|
||||||
} on http.ClientException catch (e) {
|
|
||||||
throw NetworkException(technicalDetails: 'Talk $endpoint: ${e.message}');
|
|
||||||
}
|
|
||||||
|
|
||||||
final status = data.statusCode;
|
final status = data.statusCode;
|
||||||
if (status < 200 || status >= 300) {
|
if (status < 200 || status >= 300) {
|
||||||
// Talk's OCS errors carry the real reason in the body (expired session,
|
// Talk's OCS errors carry the real reason in the body (expired session,
|
||||||
// removed participant, ...); include a trimmed preview so the dialog and
|
// removed participant, ...); include a trimmed preview so the dialog and
|
||||||
// logs surface the cause instead of just the bare status code.
|
// logs surface the cause instead of just the bare status code.
|
||||||
final body = data.body.replaceAll(RegExp(r'\s+'), ' ').trim();
|
final detail = httpErrorDetail('Talk $endpoint', data.body, status);
|
||||||
final preview = body.length > 500 ? '${body.substring(0, 500)}…' : body;
|
|
||||||
final detail = body.isEmpty
|
|
||||||
? 'Talk $endpoint -> HTTP $status'
|
|
||||||
: 'Talk $endpoint -> HTTP $status body=$preview';
|
|
||||||
log(detail);
|
log(detail);
|
||||||
if (status == 401) {
|
throwForStatus(status, detail);
|
||||||
throw AuthException.unauthorized(technicalDetails: detail);
|
|
||||||
}
|
|
||||||
if (status == 403) {
|
|
||||||
throw AuthException.forbidden(technicalDetails: detail);
|
|
||||||
}
|
|
||||||
if (status == 404) throw NotFoundException(technicalDetails: detail);
|
|
||||||
throw ServerException(statusCode: status, technicalDetails: detail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import 'package:nextcloud/nextcloud.dart';
|
|||||||
|
|
||||||
import '../../../model/account_data.dart';
|
import '../../../model/account_data.dart';
|
||||||
import '../../../model/endpoint_data.dart';
|
import '../../../model/endpoint_data.dart';
|
||||||
import '../../api_request.dart';
|
|
||||||
import '../../api_response.dart';
|
import '../../api_response.dart';
|
||||||
|
|
||||||
abstract class WebdavApi<T> extends ApiRequest {
|
abstract class WebdavApi<T> {
|
||||||
T genericParams;
|
T genericParams;
|
||||||
|
|
||||||
WebdavApi(this.genericParams) {
|
WebdavApi(this.genericParams) {
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:jiffy/jiffy.dart';
|
|
||||||
|
|
||||||
import '../api_request.dart';
|
|
||||||
import '../errors/network_exception.dart';
|
import '../errors/network_exception.dart';
|
||||||
import '../errors/parse_exception.dart';
|
import '../errors/parse_exception.dart';
|
||||||
import '../errors/server_exception.dart';
|
import '../errors/server_exception.dart';
|
||||||
|
import '../http_errors.dart';
|
||||||
|
|
||||||
abstract class MhslApi<T> extends ApiRequest {
|
abstract class MhslApi<T> {
|
||||||
String subpath;
|
String subpath;
|
||||||
MhslApi(this.subpath);
|
MhslApi(this.subpath);
|
||||||
|
|
||||||
http.Response? response;
|
|
||||||
|
|
||||||
Future<http.Response>? request(Uri uri);
|
Future<http.Response>? request(Uri uri);
|
||||||
T assemble(String raw);
|
T assemble(String raw);
|
||||||
|
|
||||||
@@ -24,23 +19,13 @@ abstract class MhslApi<T> extends ApiRequest {
|
|||||||
'https://mhsl.eu/marianum/marianummobile/$subpath',
|
'https://mhsl.eu/marianum/marianummobile/$subpath',
|
||||||
);
|
);
|
||||||
|
|
||||||
final http.Response data;
|
final data = await sendGuarded('mhsl $subpath', () => request(endpoint));
|
||||||
try {
|
if (data == null) {
|
||||||
final raw = await request(endpoint);
|
|
||||||
if (raw == null) {
|
|
||||||
throw const NetworkException(
|
throw const NetworkException(
|
||||||
userMessage: 'Keine Antwort vom MHSL-Dienst erhalten.',
|
userMessage: 'Keine Antwort vom MHSL-Dienst erhalten.',
|
||||||
technicalDetails: 'mhsl request returned null',
|
technicalDetails: 'mhsl request returned null',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
data = raw;
|
|
||||||
} on SocketException catch (e) {
|
|
||||||
throw NetworkException(technicalDetails: 'mhsl $subpath: ${e.message}');
|
|
||||||
} on TimeoutException catch (e) {
|
|
||||||
throw NetworkException.timeout(technicalDetails: 'mhsl $subpath: $e');
|
|
||||||
} on http.ClientException catch (e) {
|
|
||||||
throw NetworkException(technicalDetails: 'mhsl $subpath: ${e.message}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.statusCode > 299) {
|
if (data.statusCode > 299) {
|
||||||
throw ServerException(
|
throw ServerException(
|
||||||
@@ -55,8 +40,4 @@ abstract class MhslApi<T> extends ApiRequest {
|
|||||||
throw ParseException(technicalDetails: 'mhsl $subpath assemble: $e');
|
throw ParseException(technicalDetails: 'mhsl $subpath assemble: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String dateTimeToJson(DateTime time) =>
|
|
||||||
Jiffy.parseFromDateTime(time).format(pattern: 'yyyy-MM-dd HH:mm:ss');
|
|
||||||
static DateTime dateTimeFromJson(String time) => DateTime.parse(time);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -256,9 +256,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
if (totalTabs != _knownTotalTabs) {
|
if (totalTabs != _knownTotalTabs) {
|
||||||
var targetIndex = currentIndex;
|
var targetIndex = currentIndex;
|
||||||
if (_userOnLastTab) {
|
if (_userOnLastTab || currentIndex >= totalTabs) {
|
||||||
targetIndex = totalTabs - 1;
|
|
||||||
} else if (currentIndex >= totalTabs) {
|
|
||||||
targetIndex = totalTabs - 1;
|
targetIndex = totalTabs - 1;
|
||||||
}
|
}
|
||||||
// Replace the controller atomically: a stale index past the new
|
// Replace the controller atomically: a stale index past the new
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
|
|
||||||
abstract class DataLoader<TResult> {
|
|
||||||
final Dio dio;
|
|
||||||
DataLoader(this.dio) {
|
|
||||||
dio.options.connectTimeout = const Duration(seconds: 10);
|
|
||||||
dio.options.sendTimeout = const Duration(seconds: 30);
|
|
||||||
dio.options.receiveTimeout = const Duration(seconds: 30);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<TResult> run() async {
|
|
||||||
final response = await fetch();
|
|
||||||
try {
|
|
||||||
return assemble(
|
|
||||||
DataLoaderResult(
|
|
||||||
json: jsonDecode(response.data!),
|
|
||||||
headers: response.headers.map.map(
|
|
||||||
(key, value) => MapEntry(key, value.join(';')),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e, stack) {
|
|
||||||
log('DataLoader assemble failed', error: e, stackTrace: stack);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Response<String>> fetch();
|
|
||||||
TResult assemble(DataLoaderResult data);
|
|
||||||
}
|
|
||||||
|
|
||||||
class DataLoaderResult {
|
|
||||||
final dynamic json;
|
|
||||||
final Map<String, String> headers;
|
|
||||||
|
|
||||||
Map<String, dynamic> asMap() => json as Map<String, dynamic>;
|
|
||||||
List<dynamic> asList() => json as List<dynamic>;
|
|
||||||
List<Map<String, dynamic>> asListOfMaps() =>
|
|
||||||
asList().map((e) => e as Map<String, dynamic>).toList();
|
|
||||||
|
|
||||||
DataLoaderResult({required this.json, required this.headers});
|
|
||||||
}
|
|
||||||
@@ -19,12 +19,5 @@ abstract class LoadableState<TState> with _$LoadableState<TState> {
|
|||||||
String? statusText,
|
String? statusText,
|
||||||
}) = _LoadableState<TState>;
|
}) = _LoadableState<TState>;
|
||||||
|
|
||||||
bool _hasError() => error != null;
|
bool showContent() => data != null;
|
||||||
bool _hasData() => data != null;
|
|
||||||
|
|
||||||
bool showPrimaryLoading() => isLoading && !_hasData();
|
|
||||||
bool showBackgroundLoading() => isLoading && _hasData();
|
|
||||||
bool showErrorBar() => _hasError() && _hasData();
|
|
||||||
bool showError() => _hasError() && !_hasData();
|
|
||||||
bool showContent() => _hasData();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class DownloadManager {
|
|||||||
|
|
||||||
final Map<String, DownloadJob> _jobs = {}; // keyed by remotePath
|
final Map<String, DownloadJob> _jobs = {}; // keyed by remotePath
|
||||||
final Map<String, DownloadJob> _byTaskId = {};
|
final Map<String, DownloadJob> _byTaskId = {};
|
||||||
final Map<String, bd.DownloadTask> _taskById = {};
|
|
||||||
|
|
||||||
/// All jobs the user should currently see in the downloads tray/overview:
|
/// All jobs the user should currently see in the downloads tray/overview:
|
||||||
/// everything that is in progress or finished-but-not-yet-opened (failed
|
/// everything that is in progress or finished-but-not-yet-opened (failed
|
||||||
@@ -134,7 +133,6 @@ class DownloadManager {
|
|||||||
)..taskId = task.taskId;
|
)..taskId = task.taskId;
|
||||||
_jobs[remotePath] = job;
|
_jobs[remotePath] = job;
|
||||||
_byTaskId[task.taskId] = job;
|
_byTaskId[task.taskId] = job;
|
||||||
_taskById[task.taskId] = task;
|
|
||||||
_refreshVisible();
|
_refreshVisible();
|
||||||
|
|
||||||
final ok = await bd.FileDownloader().enqueue(task);
|
final ok = await bd.FileDownloader().enqueue(task);
|
||||||
@@ -217,10 +215,7 @@ class DownloadManager {
|
|||||||
job.status.value = const DownloadCancelled();
|
job.status.value = const DownloadCancelled();
|
||||||
}
|
}
|
||||||
_jobs.remove(job.remotePath);
|
_jobs.remove(job.remotePath);
|
||||||
if (taskId != null) {
|
if (taskId != null) _byTaskId.remove(taskId);
|
||||||
_byTaskId.remove(taskId);
|
|
||||||
_taskById.remove(taskId);
|
|
||||||
}
|
|
||||||
scheduleMicrotask(job.dispose);
|
scheduleMicrotask(job.dispose);
|
||||||
}
|
}
|
||||||
_refreshVisible();
|
_refreshVisible();
|
||||||
@@ -314,10 +309,7 @@ class DownloadManager {
|
|||||||
void _remove(DownloadJob job) {
|
void _remove(DownloadJob job) {
|
||||||
_jobs.remove(job.remotePath);
|
_jobs.remove(job.remotePath);
|
||||||
final taskId = job.taskId;
|
final taskId = job.taskId;
|
||||||
if (taskId != null) {
|
if (taskId != null) _byTaskId.remove(taskId);
|
||||||
_byTaskId.remove(taskId);
|
|
||||||
_taskById.remove(taskId);
|
|
||||||
}
|
|
||||||
_refreshVisible();
|
_refreshVisible();
|
||||||
scheduleMicrotask(job.dispose);
|
scheduleMicrotask(job.dispose);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,11 +171,9 @@ class _FileElementState extends State<FileElement>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _delete() async {
|
void _delete() {
|
||||||
if (guardDemoAction(context)) return;
|
if (guardDemoAction(context)) return;
|
||||||
await showDialog<void>(
|
ConfirmDialog(
|
||||||
context: context,
|
|
||||||
builder: (context) => ConfirmDialog(
|
|
||||||
title: 'Element löschen?',
|
title: 'Element löschen?',
|
||||||
content: 'Das Element wird unwiederruflich gelöscht.',
|
content: 'Das Element wird unwiederruflich gelöscht.',
|
||||||
confirmButton: 'Löschen',
|
confirmButton: 'Löschen',
|
||||||
@@ -184,8 +182,7 @@ class _FileElementState extends State<FileElement>
|
|||||||
await webdav.delete(PathUri.parse(widget.file.path));
|
await webdav.delete(PathUri.parse(widget.file.path));
|
||||||
widget.refetch();
|
widget.refetch();
|
||||||
},
|
},
|
||||||
),
|
).asDialog(context);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showActionSheet() {
|
void _showActionSheet() {
|
||||||
|
|||||||
@@ -86,20 +86,11 @@ class ChatBackgroundSettingsPage extends StatelessWidget {
|
|||||||
trailing: DropdownButton<ChatBackgroundType>(
|
trailing: DropdownButton<ChatBackgroundType>(
|
||||||
value: s.type,
|
value: s.type,
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
icon: const Icon(Icons.arrow_drop_down),
|
||||||
items: ChatBackgroundType.values
|
items: _iconDropdownItems(
|
||||||
.map(
|
ChatBackgroundType.values,
|
||||||
(e) => DropdownMenuItem<ChatBackgroundType>(
|
_typeIcon,
|
||||||
value: e,
|
_typeLabel,
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(_typeIcon(e)),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(_typeLabel(e)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: (e) => _onTypeChanged(context, settings, e!),
|
onChanged: (e) => _onTypeChanged(context, settings, e!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -142,20 +133,11 @@ class ChatBackgroundSettingsPage extends StatelessWidget {
|
|||||||
trailing: DropdownButton<ChatBackgroundFit>(
|
trailing: DropdownButton<ChatBackgroundFit>(
|
||||||
value: s.fit,
|
value: s.fit,
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
icon: const Icon(Icons.arrow_drop_down),
|
||||||
items: ChatBackgroundFit.values
|
items: _iconDropdownItems(
|
||||||
.map(
|
ChatBackgroundFit.values,
|
||||||
(e) => DropdownMenuItem<ChatBackgroundFit>(
|
_fitIcon,
|
||||||
value: e,
|
_fitLabel,
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(_fitIcon(e)),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(_fitLabel(e)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: (e) {
|
onChanged: (e) {
|
||||||
Haptics.selection();
|
Haptics.selection();
|
||||||
settings.val(write: true).chatBackgroundSettings.fit =
|
settings.val(write: true).chatBackgroundSettings.fit =
|
||||||
@@ -280,6 +262,27 @@ class ChatBackgroundSettingsPage extends StatelessWidget {
|
|||||||
cs.type = ChatBackgroundType.color;
|
cs.type = ChatBackgroundType.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dropdown menu items rendered as an icon + label row, shared by the source
|
||||||
|
/// and fill-mode pickers.
|
||||||
|
static List<DropdownMenuItem<T>> _iconDropdownItems<T>(
|
||||||
|
List<T> values,
|
||||||
|
IconData Function(T) icon,
|
||||||
|
String Function(T) label,
|
||||||
|
) => values
|
||||||
|
.map(
|
||||||
|
(e) => DropdownMenuItem<T>(
|
||||||
|
value: e,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon(e)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(label(e)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
IconData _typeIcon(ChatBackgroundType type) => switch (type) {
|
IconData _typeIcon(ChatBackgroundType type) => switch (type) {
|
||||||
ChatBackgroundType.pattern => Icons.texture_outlined,
|
ChatBackgroundType.pattern => Icons.texture_outlined,
|
||||||
ChatBackgroundType.image => Icons.image_outlined,
|
ChatBackgroundType.image => Icons.image_outlined,
|
||||||
@@ -330,7 +333,6 @@ class _Preview extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_bubble(
|
_bubble(
|
||||||
context,
|
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
color: remoteColor,
|
color: remoteColor,
|
||||||
text: 'Wie gefällt dir der neue Hintergrund?',
|
text: 'Wie gefällt dir der neue Hintergrund?',
|
||||||
@@ -338,7 +340,6 @@ class _Preview extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_bubble(
|
_bubble(
|
||||||
context,
|
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
color: selfColor,
|
color: selfColor,
|
||||||
text: 'Sieht richtig gut aus! 🎉',
|
text: 'Sieht richtig gut aus! 🎉',
|
||||||
@@ -352,8 +353,7 @@ class _Preview extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _bubble(
|
Widget _bubble({
|
||||||
BuildContext context, {
|
|
||||||
required Alignment alignment,
|
required Alignment alignment,
|
||||||
required Color color,
|
required Color color,
|
||||||
required String text,
|
required String text,
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ class _ChatViewState extends State<ChatView> with RouteAware {
|
|||||||
lastDate = elementDate;
|
lastDate = elementDate;
|
||||||
messages.add(
|
messages.add(
|
||||||
ChatBubble(
|
ChatBubble(
|
||||||
context: context,
|
|
||||||
isSender: false,
|
isSender: false,
|
||||||
bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
|
bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
|
||||||
chatData: widget.room,
|
chatData: widget.room,
|
||||||
@@ -265,7 +264,6 @@ class _ChatViewState extends State<ChatView> with RouteAware {
|
|||||||
|
|
||||||
messages.add(
|
messages.add(
|
||||||
ChatBubble(
|
ChatBubble(
|
||||||
context: context,
|
|
||||||
isSender:
|
isSender:
|
||||||
element.actorId == widget.selfId &&
|
element.actorId == widget.selfId &&
|
||||||
(element.messageType ==
|
(element.messageType ==
|
||||||
@@ -287,7 +285,6 @@ class _ChatViewState extends State<ChatView> with RouteAware {
|
|||||||
messages.insert(
|
messages.insert(
|
||||||
0,
|
0,
|
||||||
ChatBubble(
|
ChatBubble(
|
||||||
context: context,
|
|
||||||
isSender: false,
|
isSender: false,
|
||||||
bubbleData: GetChatResponseObject.getTextDummy(
|
bubbleData: GetChatResponseObject.getTextDummy(
|
||||||
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
|
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
|
||||||
|
|||||||
@@ -24,29 +24,23 @@ class ChatBubbleStyles {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
);
|
);
|
||||||
|
|
||||||
BubbleStyle getRemoteStyle(bool seamless) {
|
BubbleStyle getRemoteStyle() => BubbleStyle(
|
||||||
var color = AppTheme.isDarkMode(context)
|
|
||||||
? const Color(0xff202c33)
|
|
||||||
: Colors.white;
|
|
||||||
return BubbleStyle(
|
|
||||||
nip: BubbleNip.leftTop,
|
nip: BubbleNip.leftTop,
|
||||||
color: seamless ? Colors.transparent : color,
|
color: AppTheme.isDarkMode(context)
|
||||||
elevation: seamless ? 0 : 1,
|
? const Color(0xff202c33)
|
||||||
|
: Colors.white,
|
||||||
|
elevation: 1,
|
||||||
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
|
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
|
||||||
alignment: Alignment.topLeft,
|
alignment: Alignment.topLeft,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
BubbleStyle getSelfStyle(bool seamless) {
|
BubbleStyle getSelfStyle() => BubbleStyle(
|
||||||
var color = AppTheme.isDarkMode(context)
|
|
||||||
? const Color(0xff005c4b)
|
|
||||||
: const Color(0xffd3d3d3);
|
|
||||||
return BubbleStyle(
|
|
||||||
nip: BubbleNip.rightBottom,
|
nip: BubbleNip.rightBottom,
|
||||||
color: seamless ? Colors.transparent : color,
|
color: AppTheme.isDarkMode(context)
|
||||||
elevation: seamless ? 0 : 1,
|
? const Color(0xff005c4b)
|
||||||
|
: const Color(0xffd3d3d3),
|
||||||
|
elevation: 1,
|
||||||
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
|
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dar
|
|||||||
import '../data/chat_bubble_styles.dart';
|
import '../data/chat_bubble_styles.dart';
|
||||||
|
|
||||||
class AnswerReference extends StatelessWidget {
|
class AnswerReference extends StatelessWidget {
|
||||||
final BuildContext context;
|
|
||||||
final GetChatResponseObject referenceMessage;
|
final GetChatResponseObject referenceMessage;
|
||||||
final String? selfId;
|
final String? selfId;
|
||||||
const AnswerReference({
|
const AnswerReference({
|
||||||
required this.context,
|
|
||||||
required this.referenceMessage,
|
required this.referenceMessage,
|
||||||
required this.selfId,
|
required this.selfId,
|
||||||
super.key,
|
super.key,
|
||||||
@@ -20,8 +18,8 @@ class AnswerReference extends StatelessWidget {
|
|||||||
final style = ChatBubbleStyles(context);
|
final style = ChatBubbleStyles(context);
|
||||||
final isSelf = referenceMessage.actorId == selfId;
|
final isSelf = referenceMessage.actorId == selfId;
|
||||||
final accent = isSelf
|
final accent = isSelf
|
||||||
? style.getSelfStyle(false).color!.withGreen(200)
|
? style.getSelfStyle().color!.withGreen(200)
|
||||||
: style.getRemoteStyle(false).color!.withWhite(200);
|
: style.getRemoteStyle().color!.withWhite(200);
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: accent.withValues(alpha: 0.2),
|
color: accent.withValues(alpha: 0.2),
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import 'highlighted_linkify.dart';
|
|||||||
enum SearchHighlight { none, secondary, active }
|
enum SearchHighlight { none, secondary, active }
|
||||||
|
|
||||||
class ChatBubble extends StatefulWidget {
|
class ChatBubble extends StatefulWidget {
|
||||||
final BuildContext context;
|
|
||||||
final bool isSender;
|
final bool isSender;
|
||||||
final GetChatResponseObject bubbleData;
|
final GetChatResponseObject bubbleData;
|
||||||
final GetRoomResponseObject chatData;
|
final GetRoomResponseObject chatData;
|
||||||
@@ -40,7 +39,6 @@ class ChatBubble extends StatefulWidget {
|
|||||||
final SearchHighlight matchHighlight;
|
final SearchHighlight matchHighlight;
|
||||||
|
|
||||||
const ChatBubble({
|
const ChatBubble({
|
||||||
required this.context,
|
|
||||||
required this.isSender,
|
required this.isSender,
|
||||||
required this.bubbleData,
|
required this.bubbleData,
|
||||||
required this.chatData,
|
required this.chatData,
|
||||||
@@ -123,8 +121,8 @@ class _ChatBubbleState extends State<ChatBubble>
|
|||||||
base = styles.getSystemStyle();
|
base = styles.getSystemStyle();
|
||||||
} else {
|
} else {
|
||||||
base = widget.isSender
|
base = widget.isSender
|
||||||
? styles.getSelfStyle(false)
|
? styles.getSelfStyle()
|
||||||
: styles.getRemoteStyle(false);
|
: styles.getRemoteStyle();
|
||||||
}
|
}
|
||||||
switch (widget.matchHighlight) {
|
switch (widget.matchHighlight) {
|
||||||
case SearchHighlight.none:
|
case SearchHighlight.none:
|
||||||
@@ -366,7 +364,6 @@ class _BubbleContent extends StatelessWidget {
|
|||||||
bubbleData.messageType ==
|
bubbleData.messageType ==
|
||||||
GetRoomResponseObjectMessageType.comment) ...[
|
GetRoomResponseObjectMessageType.comment) ...[
|
||||||
AnswerReference(
|
AnswerReference(
|
||||||
context: context,
|
|
||||||
referenceMessage: parent!,
|
referenceMessage: parent!,
|
||||||
selfId: selfId,
|
selfId: selfId,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -263,7 +263,6 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AnswerReference(
|
child: AnswerReference(
|
||||||
context: context,
|
|
||||||
referenceMessage: referenceMessage,
|
referenceMessage: referenceMessage,
|
||||||
selfId: widget.selfId,
|
selfId: widget.selfId,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -78,39 +78,12 @@ class _OutsideDayColumn extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
void _showOverflow(BuildContext context, List<Appointment> hidden) {
|
void _showOverflow(BuildContext context, List<Appointment> hidden) {
|
||||||
showDetailsBottomSheet(
|
_showAppointmentOverflowSheet(
|
||||||
context,
|
context,
|
||||||
children: (sheetCtx) {
|
hidden,
|
||||||
final tiles = <Widget>[];
|
onAppointmentTap: onAppointmentTap,
|
||||||
for (var i = 0; i < hidden.length; i++) {
|
isCrossedOut: isCrossedOut,
|
||||||
if (i > 0) tiles.add(const Divider(height: 1));
|
subtitle: _subtitleFor,
|
||||||
final apt = hidden[i];
|
|
||||||
tiles.add(
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: apt.color,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
apt.subject,
|
|
||||||
style: isCrossedOut(apt)
|
|
||||||
? const TextStyle(decoration: TextDecoration.lineThrough)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
subtitle: Text(_subtitleFor(apt)),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(sheetCtx).pop();
|
|
||||||
onAppointmentTap(apt);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return tiles;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,39 +252,12 @@ class _DayColumn extends StatelessWidget {
|
|||||||
) {
|
) {
|
||||||
final sorted = [...appointments]
|
final sorted = [...appointments]
|
||||||
..sort((a, b) => a.startTime.compareTo(b.startTime));
|
..sort((a, b) => a.startTime.compareTo(b.startTime));
|
||||||
showDetailsBottomSheet(
|
_showAppointmentOverflowSheet(
|
||||||
context,
|
context,
|
||||||
children: (sheetContext) {
|
sorted,
|
||||||
final tiles = <Widget>[];
|
onAppointmentTap: onAppointmentTap,
|
||||||
for (var i = 0; i < sorted.length; i++) {
|
isCrossedOut: isCrossedOut,
|
||||||
if (i > 0) tiles.add(const Divider(height: 1));
|
subtitle: _overflowSubtitle,
|
||||||
final apt = sorted[i];
|
|
||||||
tiles.add(
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: apt.color,
|
|
||||||
borderRadius: BorderRadius.circular(3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
apt.subject,
|
|
||||||
style: isCrossedOut(apt)
|
|
||||||
? const TextStyle(decoration: TextDecoration.lineThrough)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
subtitle: Text(_overflowSubtitle(apt)),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(sheetContext).pop();
|
|
||||||
onAppointmentTap(apt);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return tiles;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +361,52 @@ class _DayColumn extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared bottom sheet listing hidden appointments (used by both the in-grid
|
||||||
|
/// and outside-hours overflow cells). [appointments] is rendered in the given
|
||||||
|
/// order — callers pre-sort as needed.
|
||||||
|
void _showAppointmentOverflowSheet(
|
||||||
|
BuildContext context,
|
||||||
|
List<Appointment> appointments, {
|
||||||
|
required void Function(Appointment) onAppointmentTap,
|
||||||
|
required bool Function(Appointment) isCrossedOut,
|
||||||
|
required String Function(Appointment) subtitle,
|
||||||
|
}) {
|
||||||
|
showDetailsBottomSheet(
|
||||||
|
context,
|
||||||
|
children: (sheetContext) {
|
||||||
|
final tiles = <Widget>[];
|
||||||
|
for (var i = 0; i < appointments.length; i++) {
|
||||||
|
if (i > 0) tiles.add(const Divider(height: 1));
|
||||||
|
final apt = appointments[i];
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
leading: Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: apt.color,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
apt.subject,
|
||||||
|
style: isCrossedOut(apt)
|
||||||
|
? const TextStyle(decoration: TextDecoration.lineThrough)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
subtitle: Text(subtitle(apt)),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(sheetContext).pop();
|
||||||
|
onAppointmentTap(apt);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class _CurrentTimeMarker extends StatelessWidget {
|
class _CurrentTimeMarker extends StatelessWidget {
|
||||||
final DateTime now;
|
final DateTime now;
|
||||||
final PeriodLayout layout;
|
final PeriodLayout layout;
|
||||||
|
|||||||
@@ -57,9 +57,6 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
|||||||
_calendarKey.currentState?.jumpToDate(_initialDisplayDate());
|
_calendarKey.currentState?.jumpToDate(_initialDisplayDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isOnInitialWeek() =>
|
|
||||||
widget.state.startDate == _mondayOf(_initialDisplayDate());
|
|
||||||
|
|
||||||
List<Appointment> _appointments(TimetableState state) {
|
List<Appointment> _appointments(TimetableState state) {
|
||||||
final timetableSettings = context
|
final timetableSettings = context
|
||||||
.watch<SettingsCubit>()
|
.watch<SettingsCubit>()
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ class AsyncDialogAction extends StatefulWidget {
|
|||||||
final AsyncActionCallback onConfirm;
|
final AsyncActionCallback onConfirm;
|
||||||
final String? cancelLabel;
|
final String? cancelLabel;
|
||||||
final AsyncErrorBuilder? errorBuilder;
|
final AsyncErrorBuilder? errorBuilder;
|
||||||
final ButtonStyle? confirmStyle;
|
|
||||||
|
|
||||||
const AsyncDialogAction({
|
const AsyncDialogAction({
|
||||||
required this.confirmLabel,
|
required this.confirmLabel,
|
||||||
required this.onConfirm,
|
required this.onConfirm,
|
||||||
this.cancelLabel = 'Abbrechen',
|
this.cancelLabel = 'Abbrechen',
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
this.confirmStyle,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +56,6 @@ class _AsyncDialogActionState extends State<AsyncDialogAction> {
|
|||||||
child: Text(widget.cancelLabel!),
|
child: Text(widget.cancelLabel!),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: widget.confirmStyle,
|
|
||||||
onPressed: _controller.busy
|
onPressed: _controller.busy
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../routing/app_routes.dart';
|
import '../routing/app_routes.dart';
|
||||||
|
import 'details_bottom_sheet.dart';
|
||||||
import 'file_pick.dart';
|
import 'file_pick.dart';
|
||||||
|
|
||||||
/// Result of the user's choice inside [showAvatarActionsSheet]. The sheet
|
/// Result of the user's choice inside [showAvatarActionsSheet]. The sheet
|
||||||
@@ -32,20 +33,9 @@ Future<AvatarSheetResult?> showAvatarActionsSheet(
|
|||||||
required bool allowRemove,
|
required bool allowRemove,
|
||||||
}) async {
|
}) async {
|
||||||
AvatarSheetResult? result;
|
AvatarSheetResult? result;
|
||||||
await showModalBottomSheet<void>(
|
await showDetailsBottomSheet(
|
||||||
context: context,
|
context,
|
||||||
isScrollControlled: true,
|
children: (sheetContext) => [
|
||||||
showDragHandle: true,
|
|
||||||
useSafeArea: true,
|
|
||||||
builder: (sheetContext) => SafeArea(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: 16 + MediaQuery.viewInsetsOf(sheetContext).bottom,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.photo_library_outlined),
|
leading: const Icon(Icons.photo_library_outlined),
|
||||||
title: const Text('Aus Galerie wählen'),
|
title: const Text('Aus Galerie wählen'),
|
||||||
@@ -63,10 +53,7 @@ Future<AvatarSheetResult?> showAvatarActionsSheet(
|
|||||||
leading: const Icon(Icons.photo_camera_outlined),
|
leading: const Icon(Icons.photo_camera_outlined),
|
||||||
title: const Text('Foto aufnehmen'),
|
title: const Text('Foto aufnehmen'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final bytes = await _pickAndCrop(
|
final bytes = await _pickAndCrop(sheetContext, FilePick.cameraPick);
|
||||||
sheetContext,
|
|
||||||
FilePick.cameraPick,
|
|
||||||
);
|
|
||||||
if (bytes == null || !sheetContext.mounted) return;
|
if (bytes == null || !sheetContext.mounted) return;
|
||||||
result = AvatarUploadResult(bytes);
|
result = AvatarUploadResult(bytes);
|
||||||
Navigator.of(sheetContext).pop();
|
Navigator.of(sheetContext).pop();
|
||||||
@@ -81,9 +68,7 @@ Future<AvatarSheetResult?> showAvatarActionsSheet(
|
|||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'Profilbild entfernen',
|
'Profilbild entfernen',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Theme.of(sheetContext).colorScheme.error),
|
||||||
color: Theme.of(sheetContext).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
result = const AvatarRemoveResult();
|
result = const AvatarRemoveResult();
|
||||||
@@ -92,9 +77,6 @@ Future<AvatarSheetResult?> showAvatarActionsSheet(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
import '../routing/app_routes.dart';
|
import '../routing/app_routes.dart';
|
||||||
|
import 'details_bottom_sheet.dart';
|
||||||
import 'file_pick.dart';
|
import 'file_pick.dart';
|
||||||
|
|
||||||
/// Bottom sheet with "from gallery" and "take photo" actions for choosing a
|
/// Bottom sheet with "from gallery" and "take photo" actions for choosing a
|
||||||
@@ -13,20 +14,9 @@ import 'file_pick.dart';
|
|||||||
/// cancelled everything.
|
/// cancelled everything.
|
||||||
Future<Uint8List?> showChatBackgroundPickerSheet(BuildContext context) async {
|
Future<Uint8List?> showChatBackgroundPickerSheet(BuildContext context) async {
|
||||||
Uint8List? result;
|
Uint8List? result;
|
||||||
await showModalBottomSheet<void>(
|
await showDetailsBottomSheet(
|
||||||
context: context,
|
context,
|
||||||
isScrollControlled: true,
|
children: (sheetContext) => [
|
||||||
showDragHandle: true,
|
|
||||||
useSafeArea: true,
|
|
||||||
builder: (sheetContext) => SafeArea(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: 16 + MediaQuery.viewInsetsOf(sheetContext).bottom,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.photo_library_outlined),
|
leading: const Icon(Icons.photo_library_outlined),
|
||||||
title: const Text('Aus Galerie wählen'),
|
title: const Text('Aus Galerie wählen'),
|
||||||
@@ -48,9 +38,6 @@ Future<Uint8List?> showChatBackgroundPickerSheet(BuildContext context) async {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class CacheView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CacheViewState extends State<CacheView> {
|
class _CacheViewState extends State<CacheView> {
|
||||||
final Localstore storage = Localstore.instance;
|
|
||||||
late Future<Map<String, dynamic>?> files;
|
late Future<Map<String, dynamic>?> files;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class DebugTile {
|
|||||||
context.read<SettingsCubit>().val().devToolsEnabled &&
|
context.read<SettingsCubit>().val().devToolsEnabled &&
|
||||||
(onlyInDebug ? kDebugMode : true);
|
(onlyInDebug ? kDebugMode : true);
|
||||||
|
|
||||||
Widget jsonData(Map<String, dynamic> data, {bool ignoreConfig = false}) =>
|
Widget jsonData(Map<String, dynamic> data) => callback(
|
||||||
callback(
|
|
||||||
title: 'JSON daten anzeigen',
|
title: 'JSON daten anzeigen',
|
||||||
onTab: () => JsonViewer.asDialog(context, data),
|
onTab: () => JsonViewer.asDialog(context, data),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,21 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../../utils/clipboard_helper.dart';
|
import '../../utils/clipboard_helper.dart';
|
||||||
|
|
||||||
class JsonViewer extends StatelessWidget {
|
class JsonViewer {
|
||||||
final String title;
|
|
||||||
final Map<String, dynamic> data;
|
|
||||||
|
|
||||||
const JsonViewer({super.key, required this.title, required this.data});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Scaffold(
|
|
||||||
appBar: AppBar(title: Text(title)),
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
child: Text(format(data)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
static final _encoder = const JsonEncoder.withIndent(' ');
|
static final _encoder = const JsonEncoder.withIndent(' ');
|
||||||
|
|
||||||
static String format(Map<String, dynamic> jsonInput) =>
|
static String format(Map<String, dynamic> jsonInput) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user