Compare commits
6
Commits
3e856d0b1c
...
a79b376bb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a79b376bb3 | ||
|
|
eddd294a34 | ||
|
|
c455bb1cae | ||
|
|
aca86c506a | ||
|
|
2664378b17 | ||
|
|
ee1240f71a |
No files matched your search
@@ -0,0 +1,28 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart';
|
||||
|
||||
import '../../model/endpoint_data.dart';
|
||||
import '../errors/network_exception.dart';
|
||||
import 'demo_mode.dart';
|
||||
|
||||
/// Demo sessions carry made-up credentials. Every request they sent to the
|
||||
/// real Nextcloud would count as a failed login there and, in bulk, trip the
|
||||
/// server's brute-force throttle for the whole IP (HTTP 429 for everyone
|
||||
/// behind it). Installed app-wide via [http.runWithClient].
|
||||
class DemoHttpClient extends http.BaseClient {
|
||||
final http.Client _inner = IOClient();
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||
if (DemoMode.active && isNextcloudHost(request.url)) {
|
||||
return Future.error(NetworkException.demoBlocked(request.url));
|
||||
}
|
||||
return _inner.send(request);
|
||||
}
|
||||
|
||||
@override
|
||||
void close() => _inner.close();
|
||||
|
||||
static bool isNextcloudHost(Uri url) =>
|
||||
url.host == EndpointData().nextcloud().domain;
|
||||
}
|
||||
@@ -10,9 +10,7 @@ import '../api_error.dart';
|
||||
import '../http_errors.dart';
|
||||
import '../marianumcloud/talk/talk_error.dart';
|
||||
import 'app_exception.dart';
|
||||
import 'auth_exception.dart';
|
||||
import 'network_exception.dart';
|
||||
import 'not_found_exception.dart';
|
||||
import 'parse_exception.dart';
|
||||
import 'server_exception.dart';
|
||||
import 'talk_exception.dart';
|
||||
@@ -65,23 +63,15 @@ AppException _dynamiteToAppException(DynamiteApiException error) {
|
||||
final detail = preview.isEmpty
|
||||
? 'HTTP $status'
|
||||
: 'HTTP $status body=$preview';
|
||||
switch (status) {
|
||||
case 401:
|
||||
return AuthException.unauthorized(technicalDetails: detail);
|
||||
case 403:
|
||||
return AuthException.forbidden(technicalDetails: detail);
|
||||
case 404:
|
||||
return NotFoundException(technicalDetails: detail);
|
||||
case 429:
|
||||
return ServerException(
|
||||
statusCode: status,
|
||||
userMessage:
|
||||
'Zu viele Anfragen. Bitte warte einen Moment, bevor du es erneut versuchst.',
|
||||
technicalDetails: detail,
|
||||
);
|
||||
default:
|
||||
return ServerException(statusCode: status, technicalDetails: detail);
|
||||
if (status == 429) {
|
||||
return ServerException(
|
||||
statusCode: status,
|
||||
userMessage:
|
||||
'Zu viele Anfragen. Bitte warte einen Moment, bevor du es erneut versuchst.',
|
||||
technicalDetails: detail,
|
||||
);
|
||||
}
|
||||
return exceptionForStatus(status, detail);
|
||||
}
|
||||
|
||||
String errorToUserMessage(Object? error, {String fallback = _defaultFallback}) {
|
||||
|
||||
@@ -13,4 +13,10 @@ class NetworkException extends AppException {
|
||||
'Der Server hat zu lange gebraucht. Bitte versuche es erneut.',
|
||||
technicalDetails: technicalDetails,
|
||||
);
|
||||
|
||||
/// A request a demo session must never send (see `DemoHttpClient`).
|
||||
factory NetworkException.demoBlocked(Uri url) => NetworkException(
|
||||
userMessage: 'Im Demo-Modus nicht verfügbar.',
|
||||
technicalDetails: 'demo mode blocked $url',
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'errors/app_exception.dart';
|
||||
import 'errors/auth_exception.dart';
|
||||
import 'errors/network_exception.dart';
|
||||
import 'errors/not_found_exception.dart';
|
||||
@@ -42,12 +43,23 @@ String httpErrorDetail(String label, String body, int 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);
|
||||
/// 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.
|
||||
AppException exceptionForStatus(int status, String detail) => switch (status) {
|
||||
401 => AuthException.unauthorized(technicalDetails: detail),
|
||||
403 => AuthException.forbidden(technicalDetails: detail),
|
||||
404 => NotFoundException(technicalDetails: detail),
|
||||
_ => ServerException(statusCode: status, technicalDetails: detail),
|
||||
};
|
||||
|
||||
/// Throws [exceptionForStatus].
|
||||
Never throwForStatus(int status, String detail) =>
|
||||
throw exceptionForStatus(status, detail);
|
||||
|
||||
/// Throws [exceptionForStatus] unless [response] is 2xx.
|
||||
void ensureSuccess(String label, http.Response response) {
|
||||
final status = response.statusCode;
|
||||
if (status >= 200 && status < 300) return;
|
||||
throwForStatus(status, httpErrorDetail(label, response.body, status));
|
||||
}
|
||||
@@ -18,13 +18,7 @@ class DeleteAppPassword {
|
||||
NextcloudOcs.uri('core/apppassword'),
|
||||
// An explicit header may belong to an inactive account, so the active
|
||||
// session's headers must not be required then.
|
||||
headers: authorizationHeader == null
|
||||
? NextcloudOcs.headers()
|
||||
: {
|
||||
'Accept': 'application/json',
|
||||
'OCS-APIRequest': 'true',
|
||||
'Authorization': authorizationHeader,
|
||||
},
|
||||
headers: NextcloudOcs.headers(authorization: authorizationHeader),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,23 +28,17 @@ class GetAppPassword {
|
||||
label,
|
||||
() => _client.get(
|
||||
NextcloudOcs.uri('core/getapppassword'),
|
||||
headers: {
|
||||
...NextcloudOcs.headers(),
|
||||
// Deliberately NOT the shared Authorization value: that one prefers
|
||||
// the app password, but an app password cannot mint another one —
|
||||
// this endpoint requires the real password.
|
||||
'Authorization': SessionManager()
|
||||
// Deliberately NOT the shared Authorization value: that one prefers
|
||||
// the app password, but an app password cannot mint another one —
|
||||
// this endpoint requires the real password.
|
||||
headers: NextcloudOcs.headers(
|
||||
authorization: SessionManager()
|
||||
.requireNextcloud()
|
||||
.realPasswordBasicAuthHeader,
|
||||
},
|
||||
),
|
||||
),
|
||||
))!;
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throwForStatus(
|
||||
response.statusCode,
|
||||
httpErrorDetail(label, response.body, response.statusCode),
|
||||
);
|
||||
}
|
||||
ensureSuccess(label, response);
|
||||
final json = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
final data = (json as Map)['ocs']?['data'];
|
||||
final appPassword = data is Map ? data['apppassword'] as String? : null;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
@@ -96,9 +95,8 @@ class GetUserInfo {
|
||||
uri,
|
||||
);
|
||||
try {
|
||||
final root = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data =
|
||||
(root['ocs'] as Map<String, dynamic>)['data'] as Map<String, dynamic>;
|
||||
NextcloudOcs.decode(response.body)['data'] as Map<String, dynamic>;
|
||||
return CloudUserInfo(
|
||||
userId: data['id'] as String,
|
||||
displayName: (data['displayname'] as String?) ?? '',
|
||||
|
||||
@@ -29,12 +29,7 @@ class LoginFlowApi {
|
||||
headers: {'Accept': 'application/json', 'User-Agent': userAgent},
|
||||
),
|
||||
))!;
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throwForStatus(
|
||||
response.statusCode,
|
||||
httpErrorDetail(label, response.body, response.statusCode),
|
||||
);
|
||||
}
|
||||
ensureSuccess(label, response);
|
||||
return LoginFlowInit.fromJson(
|
||||
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
|
||||
);
|
||||
@@ -53,12 +48,7 @@ class LoginFlowApi {
|
||||
),
|
||||
))!;
|
||||
if (response.statusCode == 404) return null;
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throwForStatus(
|
||||
response.statusCode,
|
||||
httpErrorDetail(label, response.body, response.statusCode),
|
||||
);
|
||||
}
|
||||
ensureSuccess(label, response);
|
||||
return LoginFlowCredentials.fromJson(
|
||||
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
@@ -13,10 +13,13 @@ class NextcloudOcs {
|
||||
static Map<String, dynamic> decode(String raw) =>
|
||||
(jsonDecode(raw) as Map<String, dynamic>)['ocs'] as Map<String, dynamic>;
|
||||
|
||||
static Map<String, String> headers() => {
|
||||
/// [authorization] overrides the active session's credentials, which are
|
||||
/// then not required.
|
||||
static Map<String, String> headers({String? authorization}) => {
|
||||
'Accept': 'application/json',
|
||||
'OCS-APIRequest': 'true',
|
||||
'Authorization': SessionManager().requireNextcloud().basicAuthHeader,
|
||||
'Authorization':
|
||||
authorization ?? SessionManager().requireNextcloud().basicAuthHeader,
|
||||
};
|
||||
|
||||
static Uri uri(String pathSuffix, {Map<String, dynamic>? queryParameters}) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../errors/server_exception.dart';
|
||||
import '../../../http_errors.dart';
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import 'get_chat_params.dart';
|
||||
import 'get_chat_response.dart';
|
||||
|
||||
/// GETs a chat page outside [TalkApi], which treats non-2xx as errors: here
|
||||
/// HTTP 304 (nothing new / no older messages) is a normal outcome and yields
|
||||
/// `null`.
|
||||
Future<GetChatResponse?> fetchChatPage({
|
||||
required String label,
|
||||
required String chatToken,
|
||||
required GetChatParams params,
|
||||
Duration? timeout,
|
||||
bool parseOffIsolate = false,
|
||||
}) async {
|
||||
final uri = NextcloudOcs.uri(
|
||||
'apps/spreed/api/v1/chat/$chatToken',
|
||||
queryParameters: params.toJson(),
|
||||
);
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final response = (await sendGuarded('$label $uri', () {
|
||||
final request = http.get(uri, headers: headers);
|
||||
return timeout == null ? request : request.timeout(timeout);
|
||||
}))!;
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status == 304) return null;
|
||||
if (status >= 200 && status < 300) {
|
||||
final parsed = parseOffIsolate
|
||||
? await compute(_parseChatResponse, response.body)
|
||||
: _parseChatResponse(response.body);
|
||||
return parsed..headers = response.headers;
|
||||
}
|
||||
throw ServerException(
|
||||
statusCode: status,
|
||||
technicalDetails: '$label $uri: HTTP $status',
|
||||
);
|
||||
}
|
||||
|
||||
GetChatResponse _parseChatResponse(String body) =>
|
||||
GetChatResponse.fromJson(NextcloudOcs.decode(body));
|
||||
@@ -1,6 +1,3 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import '../talk_api.dart';
|
||||
import 'get_chat_params.dart';
|
||||
@@ -16,11 +13,4 @@ class GetChat extends TalkApi<GetChatResponse> {
|
||||
@override
|
||||
GetChatResponse assemble(String raw) =>
|
||||
GetChatResponse.fromJson(NextcloudOcs.decode(raw));
|
||||
|
||||
@override
|
||||
Future<Response> request(
|
||||
Uri uri,
|
||||
Object? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.get(uri, headers: headers);
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../errors/server_exception.dart';
|
||||
import '../../../http_errors.dart';
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import 'fetch_chat_page.dart';
|
||||
import 'get_chat_params.dart';
|
||||
import 'get_chat_response.dart';
|
||||
|
||||
/// Backwards-paging variant of GetChat (`lookIntoFuture=0` + `lastKnownMessageId`)
|
||||
/// that fetches the page of messages *older* than a given id. Bypasses [TalkApi]
|
||||
/// because that layer treats non-2xx as errors, and the server answers HTTP 304
|
||||
/// when there are no older messages left — a normal "start of chat" outcome here.
|
||||
/// `setReadMarker=off` so paging into history never moves the read cursor.
|
||||
/// that fetches the page of messages *older* than a given id. The server
|
||||
/// answers HTTP 304 when there are no older messages left — a normal "start of
|
||||
/// chat" outcome (see [fetchChatPage]). `setReadMarker=off` so paging into
|
||||
/// history never moves the read cursor.
|
||||
class GetChatHistory {
|
||||
final String chatToken;
|
||||
final int lastKnownMessageId;
|
||||
@@ -24,39 +19,18 @@ class GetChatHistory {
|
||||
});
|
||||
|
||||
/// Returns the older page, or `null` on HTTP 304 (no older messages).
|
||||
Future<GetChatResponse?> run() async {
|
||||
final params = GetChatParams(
|
||||
Future<GetChatResponse?> run() => fetchChatPage(
|
||||
label: 'GetChatHistory',
|
||||
chatToken: chatToken,
|
||||
params: GetChatParams(
|
||||
lookIntoFuture: GetChatParamsSwitch.off,
|
||||
lastKnownMessageId: lastKnownMessageId,
|
||||
includeLastKnown: GetChatParamsSwitch.off,
|
||||
setReadMarker: GetChatParamsSwitch.off,
|
||||
limit: limit,
|
||||
);
|
||||
final uri = NextcloudOcs.uri(
|
||||
'apps/spreed/api/v1/chat/$chatToken',
|
||||
queryParameters: params.toJson(),
|
||||
);
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final response = (await sendGuarded(
|
||||
'GetChatHistory $uri',
|
||||
() => http.get(uri, headers: headers),
|
||||
))!;
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status == 304) return null;
|
||||
if (status >= 200 && status < 300) {
|
||||
// A page holds up to 200 messages and lands while the user is scrolling;
|
||||
// decoding it on the UI isolate stalls the fling.
|
||||
final parsed = await compute(_parseChatResponse, response.body);
|
||||
return parsed..headers = response.headers;
|
||||
}
|
||||
throw ServerException(
|
||||
statusCode: status,
|
||||
technicalDetails: 'GetChatHistory $uri: HTTP $status',
|
||||
);
|
||||
}
|
||||
),
|
||||
// A page holds up to 200 messages and lands while the user is scrolling;
|
||||
// decoding it on the UI isolate stalls the fling.
|
||||
parseOffIsolate: true,
|
||||
);
|
||||
}
|
||||
|
||||
GetChatResponse _parseChatResponse(String body) =>
|
||||
GetChatResponse.fromJson(NextcloudOcs.decode(body));
|
||||
@@ -70,10 +70,15 @@ class GetChatResponseObject {
|
||||
_$GetChatResponseObjectFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$GetChatResponseObjectToJson(this);
|
||||
|
||||
static GetChatResponseObject getDateDummy(int timestamp) {
|
||||
var elementDate = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
return getTextDummy(elementDate.formatDateRelativeShort());
|
||||
}
|
||||
/// [timestamp] is in seconds.
|
||||
DateTime get sentAt => DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
|
||||
static GetChatResponseObject getDateDummy(int timestamp) =>
|
||||
getTextDummy(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
timestamp * 1000,
|
||||
).formatDateRelativeShort(),
|
||||
);
|
||||
|
||||
static GetChatResponseObject getTextDummy(String text) =>
|
||||
GetChatResponseObject(
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../errors/server_exception.dart';
|
||||
import '../../../http_errors.dart';
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import 'fetch_chat_page.dart';
|
||||
import 'get_chat_params.dart';
|
||||
import 'get_chat_response.dart';
|
||||
|
||||
/// Long-poll variant of GetChat (`lookIntoFuture=1`). Bypasses [TalkApi]
|
||||
/// because that layer treats non-2xx as errors, and we need 304 to be a
|
||||
/// normal "no new messages" outcome. `setReadMarker=on` lets the server
|
||||
/// move the read cursor whenever the call returns messages.
|
||||
/// Long-poll variant of GetChat (`lookIntoFuture=1`); HTTP 304 is the normal
|
||||
/// "no new messages" outcome (see [fetchChatPage]). `setReadMarker=on` lets
|
||||
/// the server move the read cursor whenever the call returns messages.
|
||||
class LongPollChat {
|
||||
final String chatToken;
|
||||
final int lastKnownMessageId;
|
||||
@@ -22,37 +17,17 @@ class LongPollChat {
|
||||
});
|
||||
|
||||
/// Returns the response, or `null` on HTTP 304 (server timeout, nothing new).
|
||||
Future<GetChatResponse?> run() async {
|
||||
final params = GetChatParams(
|
||||
Future<GetChatResponse?> run() => fetchChatPage(
|
||||
label: 'LongPollChat',
|
||||
chatToken: chatToken,
|
||||
params: GetChatParams(
|
||||
lookIntoFuture: GetChatParamsSwitch.on,
|
||||
timeout: timeoutSeconds,
|
||||
lastKnownMessageId: lastKnownMessageId,
|
||||
includeLastKnown: GetChatParamsSwitch.off,
|
||||
setReadMarker: GetChatParamsSwitch.on,
|
||||
limit: 100,
|
||||
);
|
||||
final uri = NextcloudOcs.uri(
|
||||
'apps/spreed/api/v1/chat/$chatToken',
|
||||
queryParameters: params.toJson(),
|
||||
);
|
||||
final headers = NextcloudOcs.headers();
|
||||
|
||||
final response = (await sendGuarded(
|
||||
'LongPollChat $uri',
|
||||
() => http
|
||||
.get(uri, headers: headers)
|
||||
.timeout(Duration(seconds: timeoutSeconds + 15)),
|
||||
))!;
|
||||
|
||||
final status = response.statusCode;
|
||||
if (status == 304) return null;
|
||||
if (status >= 200 && status < 300) {
|
||||
return GetChatResponse.fromJson(NextcloudOcs.decode(response.body))
|
||||
..headers = response.headers;
|
||||
}
|
||||
throw ServerException(
|
||||
statusCode: status,
|
||||
technicalDetails: 'LongPollChat $uri: HTTP $status',
|
||||
);
|
||||
}
|
||||
),
|
||||
timeout: Duration(seconds: timeoutSeconds + 15),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import '../talk_api.dart';
|
||||
import 'get_participants_response.dart';
|
||||
@@ -11,11 +9,4 @@ class GetParticipants extends TalkApi<GetParticipantsResponse> {
|
||||
@override
|
||||
GetParticipantsResponse assemble(String raw) =>
|
||||
GetParticipantsResponse.fromJson(NextcloudOcs.decode(raw));
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
Object? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.get(uri, headers: headers);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import '../talk_api.dart';
|
||||
import 'get_poll_state_response.dart';
|
||||
@@ -13,11 +11,4 @@ class GetPollState extends TalkApi<GetPollStateResponse> {
|
||||
@override
|
||||
GetPollStateResponse assemble(String raw) =>
|
||||
GetPollStateResponse.fromJson(NextcloudOcs.decode(raw));
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
Object? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.get(uri, headers: headers);
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../../../api_params.dart';
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import '../talk_api.dart';
|
||||
import 'get_reactions_response.dart';
|
||||
@@ -15,11 +11,4 @@ class GetReactions extends TalkApi<GetReactionsResponse> {
|
||||
@override
|
||||
GetReactionsResponse assemble(String raw) =>
|
||||
GetReactionsResponse.fromJson(NextcloudOcs.decode(raw));
|
||||
|
||||
@override
|
||||
Future<Response>? request(
|
||||
Uri uri,
|
||||
ApiParams? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.get(uri, headers: headers);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import '../talk_api.dart';
|
||||
import 'get_shared_items_response.dart';
|
||||
@@ -30,11 +28,4 @@ class GetSharedItems extends TalkApi<GetSharedItemsResponse> {
|
||||
@override
|
||||
GetSharedItemsResponse assemble(String raw) =>
|
||||
GetSharedItemsResponse.fromOcs(NextcloudOcs.decode(raw));
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
Object? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.get(uri, headers: headers);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import '../talk_api.dart';
|
||||
import 'get_shared_items_overview_response.dart';
|
||||
@@ -18,11 +16,4 @@ class GetSharedItemsOverview extends TalkApi<GetSharedItemsOverviewResponse> {
|
||||
@override
|
||||
GetSharedItemsOverviewResponse assemble(String raw) =>
|
||||
GetSharedItemsOverviewResponse.fromOcs(NextcloudOcs.decode(raw));
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
Object? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.get(uri, headers: headers);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../nextcloud_ocs.dart';
|
||||
import '../talk_api.dart';
|
||||
@@ -17,13 +16,6 @@ class GetRoom extends TalkApi<GetRoomResponse> {
|
||||
@override
|
||||
Future<GetRoomResponse> assembleAsync(String raw) =>
|
||||
compute(_parseRooms, raw);
|
||||
|
||||
@override
|
||||
Future<http.Response> request(
|
||||
Uri uri,
|
||||
Object? body,
|
||||
Map<String, String>? headers,
|
||||
) => http.get(uri, headers: headers);
|
||||
}
|
||||
|
||||
GetRoomResponse _parseRooms(String raw) =>
|
||||
|
||||
@@ -23,11 +23,12 @@ abstract class TalkApi<T extends ApiResponse?> {
|
||||
|
||||
TalkApi(this.path, this.body, {this.headers, this.getParameters});
|
||||
|
||||
/// Defaults to a plain GET; endpoints with a body or another verb override it.
|
||||
Future<http.Response>? request(
|
||||
Uri uri,
|
||||
ApiParams? body,
|
||||
Map<String, String>? headers,
|
||||
);
|
||||
) => http.get(uri, headers: headers);
|
||||
T assemble(String raw);
|
||||
|
||||
/// Override to parse large payloads off the UI isolate (e.g. via `compute`
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:nextcloud/nextcloud.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../../session/session_manager.dart';
|
||||
import '../../api_response.dart';
|
||||
import '../../demo/demo_mode.dart';
|
||||
import '../../errors/network_exception.dart';
|
||||
|
||||
abstract class WebdavApi<T> {
|
||||
T genericParams;
|
||||
@@ -18,6 +20,14 @@ abstract class WebdavApi<T> {
|
||||
/// changes (app password minted/renewed, account switch) so it never keeps
|
||||
/// authenticating with stale credentials.
|
||||
static Future<WebDavClient> get webdav {
|
||||
// The WebDAV client talks dart:io directly, past DemoHttpClient.
|
||||
if (DemoMode.active) {
|
||||
return Future.error(
|
||||
NetworkException.demoBlocked(
|
||||
Uri.parse(EndpointData().nextcloud().origin()),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Keyed by user too: two accounts may share a password (no app password
|
||||
// minted), and the client would keep the previous login name.
|
||||
final nextcloud = SessionManager().requireNextcloud();
|
||||
@@ -31,7 +41,7 @@ abstract class WebdavApi<T> {
|
||||
|
||||
static Future<WebDavClient> establishWebdavConnection() async =>
|
||||
NextcloudClient(
|
||||
Uri.parse('https://${EndpointData().nextcloud().full()}'),
|
||||
Uri.parse(EndpointData().nextcloud().origin()),
|
||||
// App password preferred — with 2FA the real password is not accepted
|
||||
// by Nextcloud at all (see NextcloudCredentials.usesLoginFlow).
|
||||
password: SessionManager().requireNextcloud().secret,
|
||||
@@ -41,5 +51,5 @@ abstract class WebdavApi<T> {
|
||||
/// Builds the WebDAV download URL without embedded credentials. Callers must
|
||||
/// authenticate via the [NextcloudCredentials.authHeaders] header instead.
|
||||
static String buildWebdavUrl() =>
|
||||
'https://${EndpointData().nextcloud().full()}/remote.php/dav/files/${SessionManager().requireNextcloud().username}/';
|
||||
'${EndpointData().nextcloud().origin()}/remote.php/dav/files/${SessionManager().requireNextcloud().username}/';
|
||||
}
|
||||
@@ -81,8 +81,8 @@ class MarianumConnectTokenStorage {
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await _storage.delete(key: _tokenKey);
|
||||
await _storage.delete(key: _tokenIdKey);
|
||||
await _storage.delete(key: _expiresAtKey);
|
||||
for (final key in fieldKeys) {
|
||||
await _storage.delete(key: key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
/// Platform wire value MarianumConnect expects (`android`/`ios`), null elsewhere.
|
||||
String? clientPlatform() {
|
||||
if (Platform.isAndroid) return 'android';
|
||||
if (Platform.isIOS) return 'ios';
|
||||
return null;
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../errors/app_exception.dart';
|
||||
import '../../../errors/ticker_content_unavailable_exception.dart';
|
||||
import '../../errors/marianumconnect_error.dart';
|
||||
import '../../marianumconnect_query.dart';
|
||||
import 'get_ticker_page_response.dart';
|
||||
|
||||
@@ -14,26 +14,20 @@ import 'get_ticker_page_response.dart';
|
||||
/// 404 `{ "error": "CONTENT_UNAVAILABLE", "webUrl": ... }`; that case is mapped
|
||||
/// to a dedicated [TickerContentUnavailableException] carrying the browser
|
||||
/// fallback URL, so the detail screen can offer "open in browser" instead of a
|
||||
/// generic error. The bespoke 404 handling is why this keeps its own try/catch
|
||||
/// instead of the base [guard].
|
||||
/// generic error.
|
||||
class GetTickerPage extends MarianumConnectQuery {
|
||||
final String slug;
|
||||
|
||||
GetTickerPage(this.slug, {super.dio});
|
||||
|
||||
Future<TickerPageResponse> run() async {
|
||||
try {
|
||||
final response = await dio.get<Map<String, dynamic>>(
|
||||
endpoint('ticker/pages/${Uri.encodeComponent(slug)}'),
|
||||
);
|
||||
return TickerPageResponse.fromJson(response.data!);
|
||||
} on DioException catch (e) {
|
||||
throw _mapError(e);
|
||||
}
|
||||
}
|
||||
Future<TickerPageResponse> run() => getObject(
|
||||
'ticker/pages/${Uri.encodeComponent(slug)}',
|
||||
TickerPageResponse.fromJson,
|
||||
);
|
||||
|
||||
Object _mapError(DioException e) {
|
||||
final response = e.response;
|
||||
@override
|
||||
AppException mapError(DioException error) {
|
||||
final response = error.response;
|
||||
if (response?.statusCode == 404) {
|
||||
final body = _asMap(response?.data);
|
||||
if (body != null && body['error'] == 'CONTENT_UNAVAILABLE') {
|
||||
@@ -44,7 +38,7 @@ class GetTickerPage extends MarianumConnectQuery {
|
||||
);
|
||||
}
|
||||
}
|
||||
return mapMarianumConnectError(e);
|
||||
return super.mapError(error);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _asMap(dynamic data) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../../client_platform.dart';
|
||||
import 'report_client_error.dart';
|
||||
|
||||
/// Fire-and-forget bridge from the app's global error handlers to the
|
||||
@@ -61,7 +61,7 @@ class ClientErrorReporter {
|
||||
message: message,
|
||||
stacktrace: stacktrace,
|
||||
context: context,
|
||||
platform: _platform(),
|
||||
platform: clientPlatform(),
|
||||
appVersion: _appVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -73,10 +73,4 @@ class ClientErrorReporter {
|
||||
final normalized = message.toLowerCase().replaceAll(RegExp(r'\d+'), '#');
|
||||
return '$errorType|$normalized|${context ?? ''}';
|
||||
}
|
||||
|
||||
static String? _platform() {
|
||||
if (Platform.isAndroid) return 'android';
|
||||
if (Platform.isIOS) return 'ios';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'dart:typed_data';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../../client_platform.dart';
|
||||
import '../../marianumconnect_query.dart';
|
||||
|
||||
/// Submits user feedback to MarianumConnect (`POST me/feedback`, bearer-auth).
|
||||
@@ -27,7 +28,7 @@ class SubmitFeedback extends MarianumConnectQuery {
|
||||
'message': message,
|
||||
'screenshot': ?screenshotBase64,
|
||||
'screenshotContentType': screenshot != null ? screenshotContentType : null,
|
||||
'platform': _platform(),
|
||||
'platform': clientPlatform(),
|
||||
'appVersion': package.version,
|
||||
'appBuild': int.tryParse(package.buildNumber),
|
||||
'deviceModel': await _deviceModel(),
|
||||
@@ -35,12 +36,6 @@ class SubmitFeedback extends MarianumConnectQuery {
|
||||
);
|
||||
});
|
||||
|
||||
static String? _platform() {
|
||||
if (Platform.isAndroid) return 'android';
|
||||
if (Platform.isIOS) return 'ios';
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<String?> _deviceModel() async {
|
||||
try {
|
||||
final info = DeviceInfoPlugin();
|
||||
|
||||
+30
-35
@@ -80,6 +80,25 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shares one debounce key, so a later emit supersedes a pending publish.
|
||||
void _scheduleWidgetPublish(TimetableState data, Duration delay) {
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
final capabilitiesCubit = context.read<CapabilitiesCubit>();
|
||||
final epoch = SessionManager().sessionEpoch;
|
||||
Debouncer.debounce(
|
||||
'widgetPublish',
|
||||
delay,
|
||||
() => unawaited(
|
||||
WidgetPublisher.publishFromBlocState(
|
||||
data,
|
||||
settings: settingsCubit.val(),
|
||||
isTeacher: capabilitiesCubit.isTeacher,
|
||||
epoch: epoch,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _reportTelemetry() {
|
||||
if (!mounted) return;
|
||||
TelemetryHeartbeat.report(
|
||||
@@ -125,13 +144,18 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
final pending = await WidgetNavigation.consumePendingTimetableTap();
|
||||
if (!pending || !mounted) return;
|
||||
// `withNavBar: false` routes sit on the root navigator above the
|
||||
// bottom-nav; pop them so jumpToTab is actually visible. Stop at
|
||||
// popups so open dialogs/sheets stay alive.
|
||||
// bottom-nav; pop them so jumpToTab is actually visible.
|
||||
_popToShell();
|
||||
AppRoutes.goToTab(context, Modules.timetable);
|
||||
}
|
||||
|
||||
/// Pops full-page routes back to the tab shell, stopping at popups so open
|
||||
/// dialogs/sheets stay alive.
|
||||
void _popToShell() {
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.popUntil((route) => route.isFirst || route is PopupRoute);
|
||||
}
|
||||
AppRoutes.goToTab(context, Modules.timetable);
|
||||
}
|
||||
|
||||
void _handlePendingShare() {
|
||||
@@ -140,10 +164,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
if (share == null || ShareIntentListener.instance.isShown(share)) return;
|
||||
// A second share would otherwise leave the previous share-flow page
|
||||
// on top with stale (already-cleared) file paths.
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.popUntil((route) => route.isFirst || route is PopupRoute);
|
||||
}
|
||||
_popToShell();
|
||||
ShareIntentListener.instance.markShown(share);
|
||||
AppRoutes.openShareTarget(context, share);
|
||||
}
|
||||
@@ -166,27 +187,13 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
timetable.refresh();
|
||||
// Mirror BLoC updates into the home-screen widget without waiting
|
||||
// for the periodic background refresh.
|
||||
final settingsCubit = context.read<SettingsCubit>();
|
||||
final capabilitiesCubit = context.read<CapabilitiesCubit>();
|
||||
_timetableWidgetSync?.cancel();
|
||||
// Debounced: a refresh or week swipe emits several times in a row, and
|
||||
// only the settled state is worth mirroring.
|
||||
_timetableWidgetSync = timetable.stream.listen((state) {
|
||||
final data = state.data;
|
||||
if (data is TimetableState && !state.isLoading) {
|
||||
final epoch = SessionManager().sessionEpoch;
|
||||
Debouncer.debounce(
|
||||
'widgetPublish',
|
||||
const Duration(seconds: 1),
|
||||
() => unawaited(
|
||||
WidgetPublisher.publishFromBlocState(
|
||||
data,
|
||||
settings: settingsCubit.val(),
|
||||
isTeacher: capabilitiesCubit.isTeacher,
|
||||
epoch: epoch,
|
||||
),
|
||||
),
|
||||
);
|
||||
_scheduleWidgetPublish(data, const Duration(seconds: 1));
|
||||
}
|
||||
});
|
||||
// Initial publish in case hydrated storage already has data. The widget
|
||||
@@ -194,19 +201,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
// frames are done (a fresh bloc emit in the meantime supersedes it).
|
||||
final initialData = timetable.state.data;
|
||||
if (initialData is TimetableState) {
|
||||
final epoch = SessionManager().sessionEpoch;
|
||||
Debouncer.debounce(
|
||||
'widgetPublish',
|
||||
const Duration(seconds: 3),
|
||||
() => unawaited(
|
||||
WidgetPublisher.publishFromBlocState(
|
||||
initialData,
|
||||
settings: settingsCubit.val(),
|
||||
isTeacher: capabilitiesCubit.isTeacher,
|
||||
epoch: epoch,
|
||||
),
|
||||
),
|
||||
);
|
||||
_scheduleWidgetPublish(initialData, const Duration(seconds: 3));
|
||||
}
|
||||
unawaited(_handlePendingWidgetNavigation());
|
||||
ShareIntentListener.pending.addListener(_handlePendingShare);
|
||||
|
||||
@@ -9,15 +9,13 @@ import '../api/marianumconnect/marianumconnect_endpoint.dart';
|
||||
import '../api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_get.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms_response.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart';
|
||||
import '../api/marianumconnect/queries/timetable_get_week/timetable_get_week.dart';
|
||||
import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../session/session.dart';
|
||||
import '../extensions/date_time.dart';
|
||||
import '../session/session_manager.dart';
|
||||
import '../widget_data/widget_data_mapper.dart';
|
||||
import '../widget_data/widget_publisher.dart';
|
||||
@@ -81,16 +79,12 @@ class WidgetBackgroundTask {
|
||||
/// Throws on fetch failure so the worker path can signal a retry.
|
||||
static Future<void> runRefreshNow({bool force = false}) async {
|
||||
await WidgetSync.ensureInitialized();
|
||||
Session? session;
|
||||
try {
|
||||
// Bounded: a hanging keystore read must not stall the caller's budget
|
||||
// (FCM handler ~25s on iOS) forever.
|
||||
session = await SessionManager().waitForLoad().timeout(
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
} on TimeoutException {
|
||||
session = null;
|
||||
}
|
||||
// Bounded: a hanging keystore read must not stall the caller's budget
|
||||
// (FCM handler ~25s on iOS) forever.
|
||||
final session = await SessionManager().waitForLoad().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
if (session == null) {
|
||||
// Deliberately does NOT flip the widget to logged-out: a failed or slow
|
||||
// keychain read (locked iOS device during the 06:00 silent push) is
|
||||
@@ -143,7 +137,6 @@ void _callbackDispatcher() {
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
await WidgetSync.ensureInitialized();
|
||||
// The background isolate doesn't go through main.dart's BlocBuilder, so we
|
||||
// re-apply the endpoint the foreground last persisted. Without this the
|
||||
// dio singleton would fall back to its hardcoded live default even when
|
||||
@@ -157,9 +150,7 @@ Future<void> _refresh() async {
|
||||
// Fetch window matches the week payload's window so the widget can roll
|
||||
// forward into next week's lessons without fresh data.
|
||||
final weekStart = WidgetDataMapper.startOfCalendarWeek(now);
|
||||
final weekEndExclusive = weekStart.add(
|
||||
const Duration(days: WidgetDataMapper.weekWindowDays),
|
||||
);
|
||||
final weekEndExclusive = weekStart.addDays(WidgetDataMapper.weekWindowDays);
|
||||
|
||||
// All six requests are independent — run them concurrently so the total
|
||||
// latency is the slowest request, not the sum (matters for the push path's
|
||||
@@ -167,14 +158,11 @@ Future<void> _refresh() async {
|
||||
// mapper rather than aborting the whole refresh.
|
||||
final timetableFuture = TimetableGetWeek().run(
|
||||
from: weekStart,
|
||||
until: weekEndExclusive.subtract(const Duration(days: 1)),
|
||||
until: weekEndExclusive.subtractDays(1),
|
||||
);
|
||||
final subjectsFuture = _runOrNull<TimetableGetSubjectsResponse>(
|
||||
() => TimetableGetSubjects().run(),
|
||||
);
|
||||
final roomsFuture = _runOrNull<TimetableGetRoomsResponse>(
|
||||
() => TimetableGetRooms().run(),
|
||||
);
|
||||
final holidaysFuture = _runOrNull<TimetableGetHolidaysResponse>(
|
||||
() => TimetableGetHolidays().run(),
|
||||
);
|
||||
@@ -186,33 +174,18 @@ Future<void> _refresh() async {
|
||||
);
|
||||
final timetable = await timetableFuture;
|
||||
final subjects = await subjectsFuture;
|
||||
final rooms = await roomsFuture;
|
||||
final holidays = await holidaysFuture;
|
||||
final timegrid = await timegridFuture;
|
||||
final customEvents = await customEventsFuture;
|
||||
|
||||
final lessons = timetable.entries;
|
||||
|
||||
final [connectDouble, isTeacher] = await Future.wait([
|
||||
WidgetSync.getConnectDoubleLessons(),
|
||||
WidgetSync.getIsTeacher(),
|
||||
]);
|
||||
final dayData = WidgetDataMapper.buildDayData(
|
||||
final (day: dayData, week: weekData) = WidgetDataMapper.buildSnapshot(
|
||||
now: now,
|
||||
lessons: lessons,
|
||||
lessons: timetable.entries,
|
||||
subjects: subjects,
|
||||
rooms: rooms,
|
||||
holidays: holidays,
|
||||
timegrid: timegrid,
|
||||
customEvents: customEvents,
|
||||
connectDoubleLessons: connectDouble,
|
||||
showClassInsteadOfTeacher: isTeacher,
|
||||
);
|
||||
final weekData = WidgetDataMapper.buildWeekData(
|
||||
now: now,
|
||||
lessons: lessons,
|
||||
subjects: subjects,
|
||||
rooms: rooms,
|
||||
holidays: holidays,
|
||||
timegrid: timegrid,
|
||||
customEvents: customEvents,
|
||||
@@ -220,10 +193,7 @@ Future<void> _refresh() async {
|
||||
showClassInsteadOfTeacher: isTeacher,
|
||||
);
|
||||
|
||||
await WidgetSync.writeDayData(dayData);
|
||||
await WidgetSync.writeWeekData(weekData);
|
||||
await WidgetSync.setLoggedIn(true);
|
||||
await WidgetSync.triggerUpdate();
|
||||
await WidgetSync.publishSnapshot(day: dayData, week: weekData);
|
||||
log(
|
||||
'[widget-bg] refreshed: day=${dayData.lessons.length} '
|
||||
'week=${weekData.lessons.length}',
|
||||
|
||||
@@ -72,9 +72,7 @@ extension DateTimeFormatting on DateTime {
|
||||
|
||||
String formatDateRelativeShort({DateTime? now}) {
|
||||
final reference = now ?? DateTime.now();
|
||||
final today = DateTime(reference.year, reference.month, reference.day);
|
||||
final self = DateTime(year, month, day);
|
||||
final diff = today.difference(self).inDays;
|
||||
final diff = reference.dateOnly.difference(dateOnly).inDays;
|
||||
|
||||
if (diff == 0) return 'Heute';
|
||||
if (diff == 1) return 'Gestern';
|
||||
|
||||
+7
-1
@@ -11,11 +11,13 @@ import 'package:flutter/scheduler.dart' show timeDilation;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:loader_overlay/loader_overlay.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import 'api/demo/demo_http_client.dart';
|
||||
import 'api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
|
||||
import 'api/marianumconnect/auth/session_validator.dart';
|
||||
import 'api/marianumconnect/marianumconnect_endpoint.dart';
|
||||
@@ -117,7 +119,11 @@ void _installErrorHandlers() {
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
// Everything runs inside the zone so every package:http client created by the
|
||||
// app (Talk, avatars, image caches) goes through DemoHttpClient.
|
||||
Future<void> main() => http.runWithClient(_main, DemoHttpClient.new);
|
||||
|
||||
Future<void> _main() async {
|
||||
log('MarianumMobile started');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SessionManager().markUiEngine();
|
||||
|
||||
@@ -5,6 +5,9 @@ class Endpoint {
|
||||
Endpoint({required this.domain, this.path = ''});
|
||||
|
||||
String full() => domain + path;
|
||||
|
||||
/// `https://` + [full], without a trailing slash.
|
||||
String origin() => 'https://${full()}';
|
||||
}
|
||||
|
||||
class EndpointData {
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../api/marianumcloud/talk/share_files_to_chat.dart';
|
||||
@@ -53,7 +52,7 @@ abstract final class SessionWipe {
|
||||
/// unregister it).
|
||||
static Future<void> accountLeft() async {
|
||||
await _stepAsync('download files', () async {
|
||||
final dir = Directory('${(await getTemporaryDirectory()).path}/downloads');
|
||||
final dir = await DownloadManager.downloadsDirectory();
|
||||
if (dir.existsSync()) await dir.delete(recursive: true);
|
||||
});
|
||||
await _stepAsync(
|
||||
|
||||
@@ -15,7 +15,7 @@ import '../widget/info_dialog.dart';
|
||||
import 'notification_tasks.dart';
|
||||
|
||||
/// Bridges FCM lifecycle callbacks to the push pipeline. Background messages are
|
||||
/// handled directly by [PushMessageHandler.onBackgroundMessage]; this class
|
||||
/// handled directly by `pushOnBackgroundMessage`; this class
|
||||
/// covers the foreground and app-opened paths where a [BuildContext] is
|
||||
/// available.
|
||||
class NotificationController {
|
||||
|
||||
@@ -27,17 +27,13 @@ class NotificationTasks {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-chat tag scheme — canonical definition lives beside the stacked
|
||||
/// notification id in chat_thread_store.dart.
|
||||
static String chatTag(String chatToken) => chatNotificationTag(chatToken);
|
||||
|
||||
/// Removes tray notifications belonging to [chatToken]. Eraser handles
|
||||
/// iOS (where the plugin's `getActiveNotifications` returns null ids
|
||||
/// for FCM posts and can't cancel them); the local-notifications sweep
|
||||
/// handles Android and acts as a fallback while Eraser's native side
|
||||
/// isn't built in yet.
|
||||
static Future<void> clearNotificationsForChat(String chatToken) async {
|
||||
final tag = chatTag(chatToken);
|
||||
final tag = chatNotificationTag(chatToken);
|
||||
try {
|
||||
await Eraser.clearAppNotificationsByTag(tag);
|
||||
} on MissingPluginException {
|
||||
|
||||
@@ -6,13 +6,20 @@ import 'package:localstore/localstore.dart';
|
||||
/// notification, small enough to stay cheap in the background isolate.
|
||||
const int kChatThreadCap = 15;
|
||||
|
||||
/// Canonical per-chat notification tag (kept in sync with
|
||||
/// `NotificationTasks.chatTag`, which delegates here).
|
||||
/// Canonical per-chat notification tag.
|
||||
String chatNotificationTag(String chatToken) => 'talk_$chatToken';
|
||||
|
||||
/// Deterministic non-negative 31-bit notification id per chat, so every
|
||||
/// message of the same conversation updates ONE stacked notification instead
|
||||
/// of adding a new card per push.
|
||||
final _fileSafeToken = RegExp(r'^[A-Za-z0-9_-]+$');
|
||||
|
||||
/// File-/doc-safe name for a chat token: normally URL-safe and used verbatim,
|
||||
/// anything else is base64url-encoded so exotic ids can't escape a directory.
|
||||
String fileSafeToken(String chatToken) => _fileSafeToken.hasMatch(chatToken)
|
||||
? chatToken
|
||||
: base64Url.encode(utf8.encode(chatToken)).replaceAll('=', '');
|
||||
|
||||
int stableChatNotificationId(String chatToken) {
|
||||
var hash = 0;
|
||||
for (final unit in chatToken.codeUnits) {
|
||||
@@ -128,12 +135,8 @@ class ChatThreadStore {
|
||||
|
||||
ChatThreadStore({Localstore? db}) : _db = db ?? Localstore.instance;
|
||||
|
||||
/// Localstore doc ids become file names — tokens are normally URL-safe and
|
||||
/// used verbatim, anything else is base64url-encoded.
|
||||
static String docIdForToken(String chatToken) =>
|
||||
RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(chatToken)
|
||||
? chatToken
|
||||
: base64Url.encode(utf8.encode(chatToken)).replaceAll('=', '');
|
||||
/// Localstore doc ids become file names.
|
||||
static String docIdForToken(String chatToken) => fileSafeToken(chatToken);
|
||||
|
||||
Future<List<ThreadMessage>> messages(String chatToken) async {
|
||||
final data = await _doc(chatToken).get();
|
||||
@@ -145,16 +148,6 @@ class ChatThreadStore {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Appends [message] and returns the updated (capped) history.
|
||||
Future<List<ThreadMessage>> append(
|
||||
String chatToken,
|
||||
ThreadMessage message,
|
||||
) async {
|
||||
final updated = appendThreadMessage(await messages(chatToken), message);
|
||||
await _write(chatToken, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// Appends [message], but restarts the thread first when the chat's previous
|
||||
/// notification is no longer active ([isActive] == false — dismissed/read).
|
||||
/// See [threadAfterIncoming] for the tri-state semantics.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:ui';
|
||||
|
||||
@@ -13,6 +12,7 @@ import '../session/session_manager.dart';
|
||||
import 'chat_thread_store.dart';
|
||||
import 'nid_store.dart';
|
||||
import 'push_renderer.dart';
|
||||
import 'push_target.dart';
|
||||
|
||||
/// Notification action identifiers shared between the renderer (which attaches
|
||||
/// the actions) and the response handlers (which dispatch them).
|
||||
@@ -93,8 +93,7 @@ class PushActions {
|
||||
// the mark-read action does. Best effort: a failure here must not
|
||||
// fail the already-delivered reply.
|
||||
await markRead(chatToken);
|
||||
}
|
||||
if (!result.ok) {
|
||||
} else {
|
||||
// Never swallow the typed message: surface the failure (and the
|
||||
// undelivered text) as its own notification. The nid mapping stays
|
||||
// alive — the thread notification is still in the tray.
|
||||
@@ -287,14 +286,6 @@ class PushActions {
|
||||
return raw == null ? null : int.tryParse(raw);
|
||||
}
|
||||
|
||||
static String? _payloadField(String? payload, String key) {
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
try {
|
||||
final map = jsonDecode(payload) as Map<String, dynamic>;
|
||||
final value = map[key];
|
||||
return value?.toString();
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
static String? _payloadField(String? payload, String key) =>
|
||||
decodeNotificationPayload(payload)?[key]?.toString();
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../api/marianumcloud/nextcloud_ocs.dart';
|
||||
import 'chat_thread_store.dart';
|
||||
|
||||
/// Result of an avatar lookup at render time: [icon] when bytes are available
|
||||
/// within the fetch budget, otherwise [late] carries the still-running fetch
|
||||
@@ -102,13 +103,7 @@ class PushAvatarStore {
|
||||
},
|
||||
);
|
||||
|
||||
/// File-safe cache name for a chat token. Tokens are normally URL-safe
|
||||
/// already and used verbatim; anything else is base64url-encoded so exotic
|
||||
/// ids can never escape the cache directory.
|
||||
static String fileNameForToken(String token) =>
|
||||
RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(token)
|
||||
? token
|
||||
: base64Url.encode(utf8.encode(token)).replaceAll('=', '');
|
||||
static String fileNameForToken(String token) => fileSafeToken(token);
|
||||
|
||||
/// Whether a cache file written at [modified] is still fresh at [now].
|
||||
static bool isFresh(DateTime modified, DateTime now) =>
|
||||
|
||||
@@ -101,7 +101,6 @@ class PushMessageHandler {
|
||||
case PushKind.connect:
|
||||
await _handleConnect(
|
||||
message,
|
||||
foreground: foreground,
|
||||
notificationsEnabled: notificationsEnabled,
|
||||
);
|
||||
break;
|
||||
@@ -139,7 +138,6 @@ class PushMessageHandler {
|
||||
|
||||
Future<void> _handleConnect(
|
||||
RemoteMessage message, {
|
||||
required bool foreground,
|
||||
required bool notificationsEnabled,
|
||||
}) async {
|
||||
// Connect pushes carry no silent side effects, so nothing to do when the
|
||||
|
||||
@@ -60,7 +60,7 @@ class PushRegistration {
|
||||
|
||||
/// Nextcloud origin the registration targets (full origin, no trailing
|
||||
/// slash) — persisted alongside the registration to detect endpoint changes.
|
||||
String get currentNcBaseUrl => 'https://${EndpointData().nextcloud().full()}';
|
||||
String get currentNcBaseUrl => EndpointData().nextcloud().origin();
|
||||
|
||||
NextcloudCredentials? get _nextcloudOrNull =>
|
||||
SessionManager().current?.nextcloud;
|
||||
@@ -258,10 +258,9 @@ class PushRegistration {
|
||||
/// Flutter engine. Best-effort — a failure here must not abort registration.
|
||||
Future<void> _persistNativeAuthContext() async {
|
||||
try {
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
await _store.saveNativeAuthContext(
|
||||
username: SessionManager().requireNextcloud().username,
|
||||
baseUrl: 'https://${endpoint.full()}',
|
||||
baseUrl: currentNcBaseUrl,
|
||||
);
|
||||
} on Object catch (e) {
|
||||
log('Push: could not persist native auth context: $e');
|
||||
|
||||
+26
-23
@@ -203,16 +203,10 @@ class PushRenderer {
|
||||
messages: styleMessages,
|
||||
);
|
||||
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
category: AndroidNotificationCategory.message,
|
||||
color: _accentColor,
|
||||
final androidDetails = _talkAndroidDetails(
|
||||
tag: tag,
|
||||
style: messagingStyle,
|
||||
silent: !alert,
|
||||
styleInformation: messagingStyle,
|
||||
actions: _talkActions,
|
||||
);
|
||||
|
||||
@@ -247,17 +241,11 @@ class PushRenderer {
|
||||
title: senderName,
|
||||
body: messageText,
|
||||
notificationDetails: NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
category: AndroidNotificationCategory.message,
|
||||
color: _accentColor,
|
||||
android: _talkAndroidDetails(
|
||||
tag: tag,
|
||||
// Single sender ⇒ no conversationTitle (1:1 convention): the sender
|
||||
// Person is the header, a title would repeat the name per line.
|
||||
styleInformation: MessagingStyleInformation(
|
||||
style: MessagingStyleInformation(
|
||||
const Person(key: 'self', name: 'Ich'),
|
||||
groupConversation: false,
|
||||
messages: [
|
||||
@@ -271,6 +259,25 @@ class PushRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
static AndroidNotificationDetails _talkAndroidDetails({
|
||||
required String tag,
|
||||
required StyleInformation style,
|
||||
AndroidNotificationCategory? category = AndroidNotificationCategory.message,
|
||||
bool silent = false,
|
||||
List<AndroidNotificationAction>? actions,
|
||||
}) => AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
category: category,
|
||||
color: _accentColor,
|
||||
tag: tag,
|
||||
silent: silent,
|
||||
styleInformation: style,
|
||||
actions: actions,
|
||||
);
|
||||
|
||||
// Both actions keep cancelNotification: true (the default): the plugin's
|
||||
// Java receiver then removes the card NATIVELY the moment the action fires.
|
||||
// The reply action must not rely on our Dart-side cancel instead — MIUI/
|
||||
@@ -306,14 +313,10 @@ class PushRenderer {
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
talkChannelId,
|
||||
talkChannelName,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
color: _accentColor,
|
||||
android: _talkAndroidDetails(
|
||||
tag: 'talk_error_$chatToken',
|
||||
styleInformation: BigTextStyleInformation(body),
|
||||
style: BigTextStyleInformation(body),
|
||||
category: null,
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -25,7 +24,7 @@ class PushTapRouter {
|
||||
PushActions.handleBackgroundResponse(response);
|
||||
return;
|
||||
}
|
||||
final map = _payloadMap(response.payload);
|
||||
final map = decodeNotificationPayload(response.payload);
|
||||
if (map == null) return;
|
||||
final target = resolvePushTarget(map);
|
||||
if (target != null) pendingTarget.value = target;
|
||||
@@ -50,13 +49,4 @@ class PushTapRouter {
|
||||
log('Reading the notification launch details failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _payloadMap(String? payload) {
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
try {
|
||||
return jsonDecode(payload) as Map<String, dynamic>;
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,16 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Decodes the JSON payload of a locally rendered notification; null when
|
||||
/// absent or malformed.
|
||||
Map<String, dynamic>? decodeNotificationPayload(String? payload) {
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
try {
|
||||
return jsonDecode(payload) as Map<String, dynamic>;
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a tapped notification leads.
|
||||
sealed class PushTarget {
|
||||
const PushTarget();
|
||||
|
||||
+34
-32
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -20,6 +21,7 @@ import '../state/app/modules/marianum_message/bloc/marianum_message_state.dart';
|
||||
import '../utils/url_opener.dart';
|
||||
import '../view/login/nextcloud_login_flow_page.dart';
|
||||
import '../view/pages/files/files.dart';
|
||||
import '../view/pages/files/files_upload_dialog.dart';
|
||||
import '../view/pages/files/sharing/sharee_picker_page.dart';
|
||||
import '../view/pages/foreign_timetable/element_picker_page.dart';
|
||||
import '../view/pages/marianum_message/marianum_message_view.dart';
|
||||
@@ -145,19 +147,17 @@ class AppRoutes {
|
||||
/// the selected element (or null if dismissed). The timetable view renders
|
||||
/// the chosen plan inline. Gated behind the `viewForeignTimetables`
|
||||
/// capability at the call site.
|
||||
static Future<TimetableElementRef?> openElementPicker(
|
||||
static Future<TimetableElementRef?> openElementPicker(BuildContext context) =>
|
||||
_pushForResult(context, const ElementPickerPage());
|
||||
|
||||
// pushScreen casts its internal MaterialPageRoute to `Route<T>`, which blows
|
||||
// up for a concrete (record) T — so push untyped (T = dynamic, like every
|
||||
// other caller) and cast the popped result ourselves.
|
||||
static Future<T?> _pushForResult<T>(
|
||||
BuildContext context,
|
||||
) async {
|
||||
// pushScreen casts its internal MaterialPageRoute to `Route<T>`, which
|
||||
// blows up for a concrete (record) T — so push untyped (T = dynamic, like
|
||||
// every other caller) and cast the popped result ourselves.
|
||||
final result = await pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: const ElementPickerPage(),
|
||||
);
|
||||
return result as TimetableElementRef?;
|
||||
}
|
||||
Widget screen,
|
||||
) async =>
|
||||
(await pushScreen(context, withNavBar: false, screen: screen)) as T?;
|
||||
|
||||
/// Opens the user/group picker for sharing a file and resolves to the chosen
|
||||
/// recipient (or null if dismissed). Which kinds are offered is gated by the
|
||||
@@ -166,19 +166,10 @@ class AppRoutes {
|
||||
BuildContext context, {
|
||||
required bool allowUsers,
|
||||
required bool allowGroups,
|
||||
}) async {
|
||||
// Push untyped (T = dynamic) and cast the popped result ourselves —
|
||||
// pushScreen can't carry a concrete non-Widget T (see openElementPicker).
|
||||
final result = await pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: ShareePickerPage(
|
||||
allowUsers: allowUsers,
|
||||
allowGroups: allowGroups,
|
||||
),
|
||||
);
|
||||
return result as ShareeRef?;
|
||||
}
|
||||
}) => _pushForResult(
|
||||
context,
|
||||
ShareePickerPage(allowUsers: allowUsers, allowGroups: allowGroups),
|
||||
);
|
||||
|
||||
static void openMarianumMessage(
|
||||
BuildContext context,
|
||||
@@ -290,6 +281,23 @@ class AppRoutes {
|
||||
pushScreen(context, withNavBar: false, screen: const FeedbackDialog());
|
||||
}
|
||||
|
||||
static Future<void> openFilesUpload(
|
||||
BuildContext context, {
|
||||
required List<String> filePaths,
|
||||
required String remotePath,
|
||||
required void Function(List<String> uploadedFilePaths) onUploadFinished,
|
||||
bool uniqueNames = false,
|
||||
}) => pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: filePaths,
|
||||
remotePath: remotePath,
|
||||
onUploadFinished: onUploadFinished,
|
||||
uniqueNames: uniqueNames,
|
||||
),
|
||||
);
|
||||
|
||||
static Future<void> openCacheView(BuildContext context) =>
|
||||
pushScreen(context, withNavBar: false, screen: const CacheView());
|
||||
|
||||
@@ -452,13 +460,7 @@ class AppRoutes {
|
||||
static GetRoomResponseObject? _findRoomByToken(
|
||||
GetRoomResponse? rooms,
|
||||
String token,
|
||||
) {
|
||||
if (rooms == null) return null;
|
||||
for (final room in rooms.data) {
|
||||
if (room.token == token) return room;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
) => rooms?.data.firstWhereOrNull((room) => room.token == token);
|
||||
|
||||
/// Pushes a module from the "Mehr" tab list. Modules already in the bottom
|
||||
/// bar are switched to via [goToTab] instead.
|
||||
|
||||
@@ -7,8 +7,8 @@ import 'remote_file_ref.dart';
|
||||
|
||||
/// Server-side WebDAV copy of [source] into [targetFolderPath]. On a 412
|
||||
/// conflict the user is asked whether to overwrite; on confirmation the call
|
||||
/// is retried with `overwrite: true`. Returns true when the file ended up at
|
||||
/// the target, false when the user cancelled.
|
||||
/// is retried with `overwrite: true` inside the dialog. Returns true when the
|
||||
/// file ended up at the target, false when the user cancelled.
|
||||
Future<bool> copyRemoteFileTo({
|
||||
required BuildContext context,
|
||||
required RemoteFileRef source,
|
||||
@@ -27,19 +27,11 @@ Future<bool> copyRemoteFileTo({
|
||||
} on DynamiteApiException catch (e) {
|
||||
if (e.statusCode != 412) rethrow;
|
||||
if (!context.mounted) return false;
|
||||
final overwrite = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => ConfirmDialog(
|
||||
title: 'Datei existiert bereits',
|
||||
content:
|
||||
'"${source.name}" existiert in /$targetFolderPath. Überschreiben?',
|
||||
confirmButton: 'Überschreiben',
|
||||
cancelButton: 'Abbrechen',
|
||||
onConfirm: () => Navigator.of(ctx).pop(true),
|
||||
),
|
||||
);
|
||||
if (overwrite != true) return false;
|
||||
await webdav.copy(src, dstUri, overwrite: true);
|
||||
return true;
|
||||
return ConfirmDialog(
|
||||
title: 'Datei existiert bereits',
|
||||
content: '"${source.name}" existiert in /$targetFolderPath. Überschreiben?',
|
||||
confirmButton: 'Überschreiben',
|
||||
onConfirmAsync: () => webdav.copy(src, dstUri, overwrite: true),
|
||||
).ask(context);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ class PendingShare {
|
||||
|
||||
bool get hasFiles => filePaths.isNotEmpty;
|
||||
bool get hasText => text != null && text!.isNotEmpty;
|
||||
bool get isEmpty => !hasFiles && !hasText;
|
||||
|
||||
/// True when [other] carries the same payload. The iOS Share Extension
|
||||
/// fires two `open(url)` requests per share (see ShareViewController), so
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'loadable_state_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'loadable_state_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
@@ -25,16 +26,21 @@ $LoadableStateStateCopyWith<LoadableStateState> get copyWith => _$LoadableStateS
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableStateState&&const DeepCollectionEquality().equals(other.connections, connections));
|
||||
final _this = this as LoadableStateState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableStateState&&const DeepCollectionEquality().equals(other.connections, _this.connections));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(connections));
|
||||
int get hashCode {
|
||||
final _this = this as LoadableStateState;
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.connections));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableStateState(connections: $connections)';
|
||||
final _this = this as LoadableStateState;
|
||||
return 'LoadableStateState(connections: ${_this.connections})';
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +69,7 @@ class _$LoadableStateStateCopyWithImpl<$Res>
|
||||
/// Create a copy of LoadableStateState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? connections = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(LoadableStateState(
|
||||
connections: freezed == connections ? _self.connections : connections // ignore: cast_nullable_to_non_nullable
|
||||
as List<ConnectivityResult>?,
|
||||
));
|
||||
@@ -206,7 +212,7 @@ return $default(_that.connections);case _:
|
||||
|
||||
|
||||
class _LoadableStateState implements LoadableStateState {
|
||||
const _LoadableStateState({required final List<ConnectivityResult>? connections}): _connections = connections;
|
||||
const _LoadableStateState({required List<ConnectivityResult>? connections}): _connections = connections;
|
||||
|
||||
|
||||
final List<ConnectivityResult>? _connections;
|
||||
@@ -229,16 +235,18 @@ _$LoadableStateStateCopyWith<_LoadableStateState> get copyWith => __$LoadableSta
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableStateState&&const DeepCollectionEquality().equals(other._connections, _connections));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableStateState&&const DeepCollectionEquality().equals(other.connections, _connections));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_connections));
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_connections));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableStateState(connections: $connections)';
|
||||
return 'LoadableStateState(connections: $connections)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'loadable_state.dart';
|
||||
@@ -9,14 +9,13 @@ part of 'loadable_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$LoadableState<TState> {
|
||||
|
||||
bool get isLoading; TState? get data; int? get lastFetch; void Function()? get reFetch; LoadingError? get error;// Transient progress note under the primary loading spinner (e.g. retry
|
||||
// progress). Lives only within one fetch cycle — never persisted.
|
||||
String? get statusText;
|
||||
bool get isLoading; TState? get data; int? get lastFetch; void Function()? get reFetch; LoadingError? get error; String? get statusText;
|
||||
/// Create a copy of LoadableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -27,16 +26,21 @@ $LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith => _$Loadable
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error)&&(identical(other.statusText, statusText) || other.statusText == statusText));
|
||||
final _this = this as LoadableState<TState>;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableState<TState>&&(identical(other.isLoading, _this.isLoading) || other.isLoading == _this.isLoading)&&const DeepCollectionEquality().equals(other.data, _this.data)&&(identical(other.lastFetch, _this.lastFetch) || other.lastFetch == _this.lastFetch)&&(identical(other.reFetch, _this.reFetch) || other.reFetch == _this.reFetch)&&(identical(other.error, _this.error) || other.error == _this.error)&&(identical(other.statusText, _this.statusText) || other.statusText == _this.statusText));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error,statusText);
|
||||
int get hashCode {
|
||||
final _this = this as LoadableState<TState>;
|
||||
return Object.hash(runtimeType,_this.isLoading,const DeepCollectionEquality().hash(_this.data),_this.lastFetch,_this.reFetch,_this.error,_this.statusText);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error, statusText: $statusText)';
|
||||
final _this = this as LoadableState<TState>;
|
||||
return 'LoadableState<$TState>(isLoading: ${_this.isLoading}, data: ${_this.data}, lastFetch: ${_this.lastFetch}, reFetch: ${_this.reFetch}, error: ${_this.error}, statusText: ${_this.statusText})';
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +69,7 @@ class _$LoadableStateCopyWithImpl<TState,$Res>
|
||||
/// Create a copy of LoadableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? data = freezed,Object? lastFetch = freezed,Object? reFetch = freezed,Object? error = freezed,Object? statusText = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(LoadableState(
|
||||
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as TState?,lastFetch: freezed == lastFetch ? _self.lastFetch : lastFetch // ignore: cast_nullable_to_non_nullable
|
||||
@@ -233,8 +237,6 @@ class _LoadableState<TState> extends LoadableState<TState> {
|
||||
@override final int? lastFetch;
|
||||
@override final void Function()? reFetch;
|
||||
@override final LoadingError? error;
|
||||
// Transient progress note under the primary loading spinner (e.g. retry
|
||||
// progress). Lives only within one fetch cycle — never persisted.
|
||||
@override final String? statusText;
|
||||
|
||||
/// Create a copy of LoadableState
|
||||
@@ -247,16 +249,18 @@ _$LoadableStateCopyWith<TState, _LoadableState<TState>> get copyWith => __$Loada
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error)&&(identical(other.statusText, statusText) || other.statusText == statusText));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error)&&(identical(other.statusText, statusText) || other.statusText == statusText));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error,statusText);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error,statusText);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error, statusText: $statusText)';
|
||||
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error, statusText: $statusText)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'loading_error.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'loading_error.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
@@ -25,16 +26,21 @@ $LoadingErrorCopyWith<LoadingError> get copyWith => _$LoadingErrorCopyWithImpl<L
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadingError&&(identical(other.message, message) || other.message == message)&&(identical(other.technicalDetails, technicalDetails) || other.technicalDetails == technicalDetails)&&(identical(other.allowRetry, allowRetry) || other.allowRetry == allowRetry));
|
||||
final _this = this as LoadingError;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadingError&&(identical(other.message, _this.message) || other.message == _this.message)&&(identical(other.technicalDetails, _this.technicalDetails) || other.technicalDetails == _this.technicalDetails)&&(identical(other.allowRetry, _this.allowRetry) || other.allowRetry == _this.allowRetry));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message,technicalDetails,allowRetry);
|
||||
int get hashCode {
|
||||
final _this = this as LoadingError;
|
||||
return Object.hash(runtimeType,_this.message,_this.technicalDetails,_this.allowRetry);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadingError(message: $message, technicalDetails: $technicalDetails, allowRetry: $allowRetry)';
|
||||
final _this = this as LoadingError;
|
||||
return 'LoadingError(message: ${_this.message}, technicalDetails: ${_this.technicalDetails}, allowRetry: ${_this.allowRetry})';
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +69,7 @@ class _$LoadingErrorCopyWithImpl<$Res>
|
||||
/// Create a copy of LoadingError
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? message = null,Object? technicalDetails = freezed,Object? allowRetry = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(LoadingError(
|
||||
message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,technicalDetails: freezed == technicalDetails ? _self.technicalDetails : technicalDetails // ignore: cast_nullable_to_non_nullable
|
||||
as String?,allowRetry: null == allowRetry ? _self.allowRetry : allowRetry // ignore: cast_nullable_to_non_nullable
|
||||
@@ -225,16 +231,18 @@ _$LoadingErrorCopyWith<_LoadingError> get copyWith => __$LoadingErrorCopyWithImp
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadingError&&(identical(other.message, message) || other.message == message)&&(identical(other.technicalDetails, technicalDetails) || other.technicalDetails == technicalDetails)&&(identical(other.allowRetry, allowRetry) || other.allowRetry == allowRetry));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadingError&&(identical(other.message, message) || other.message == message)&&(identical(other.technicalDetails, technicalDetails) || other.technicalDetails == technicalDetails)&&(identical(other.allowRetry, allowRetry) || other.allowRetry == allowRetry));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message,technicalDetails,allowRetry);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,message,technicalDetails,allowRetry);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadingError(message: $message, technicalDetails: $technicalDetails, allowRetry: $allowRetry)';
|
||||
return 'LoadingError(message: $message, technicalDetails: $technicalDetails, allowRetry: $allowRetry)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class LoadableStateConsumer<
|
||||
super.key,
|
||||
});
|
||||
|
||||
static Duration animationDuration = const Duration(milliseconds: 200);
|
||||
static const Duration animationDuration = Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../widget/info_dialog.dart';
|
||||
import '../bloc/loadable_state_bloc.dart';
|
||||
import 'loadable_state_consumer.dart';
|
||||
|
||||
class LoadableStateErrorBar extends StatelessWidget {
|
||||
final bool visible;
|
||||
@@ -21,8 +22,6 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Duration animationDuration = const Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
@@ -31,9 +30,9 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
final shouldShow = visible || isOfflineWithCache;
|
||||
|
||||
return AnimatedSize(
|
||||
duration: animationDuration,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
child: AnimatedSwitcher(
|
||||
duration: animationDuration,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
transitionBuilder: (Widget child, Animation<double> animation) =>
|
||||
SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
@@ -46,35 +45,27 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
key: Key(shouldShow.hashCode.toString()),
|
||||
visible: shouldShow,
|
||||
replacement: const SizedBox(width: double.infinity),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
var bloc = context.watch<LoadableStateBloc>();
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (!bloc.isConnected()) return;
|
||||
final body = [
|
||||
if (message != null && message!.isNotEmpty) message!,
|
||||
if (technicalDetails != null &&
|
||||
technicalDetails!.isNotEmpty)
|
||||
technicalDetails!,
|
||||
].join('\n\n');
|
||||
if (body.isEmpty) return;
|
||||
InfoDialog.show(
|
||||
context,
|
||||
body,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: bloc.connectionColor(context),
|
||||
),
|
||||
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (!bloc.isConnected()) return;
|
||||
final body = [
|
||||
if (message != null && message!.isNotEmpty) message!,
|
||||
if (technicalDetails != null && technicalDetails!.isNotEmpty)
|
||||
technicalDetails!,
|
||||
].join('\n\n');
|
||||
if (body.isEmpty) return;
|
||||
InfoDialog.show(
|
||||
context,
|
||||
body,
|
||||
copyable: true,
|
||||
title: 'Fehlerdetails',
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 20,
|
||||
decoration: BoxDecoration(color: bloc.connectionColor(context)),
|
||||
child: LoadableStateErrorBarText(lastUpdated: lastUpdated),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -106,8 +97,9 @@ class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var bloc = context.watch<LoadableStateBloc>();
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
final foreground = bloc.connectionForegroundColor(context);
|
||||
final text = bloc.connectionText(lastUpdated: widget.lastUpdated);
|
||||
|
||||
// liveRegion, damit das Auftauchen des Offline-/Fehlerbanners angesagt wird;
|
||||
// Row-Semantik ausgeschlossen (Icon + Text) und Text über das explizite
|
||||
@@ -115,17 +107,14 @@ class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
|
||||
return Semantics(
|
||||
liveRegion: true,
|
||||
container: true,
|
||||
label: bloc.connectionText(lastUpdated: widget.lastUpdated),
|
||||
label: text,
|
||||
child: ExcludeSemantics(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(bloc.connectionIcon(), size: 14, color: foreground),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
bloc.connectionText(lastUpdated: widget.lastUpdated),
|
||||
style: TextStyle(fontSize: 12, color: foreground),
|
||||
),
|
||||
Text(text, style: TextStyle(fontSize: 12, color: foreground)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
-1
@@ -253,7 +253,6 @@ abstract class LoadableHydratedBloc<
|
||||
/// `==`, so use fields that compare cheaply, e.g. by identity).
|
||||
Object? persistenceKey(TState data) => data;
|
||||
|
||||
|
||||
Future<void> gatherData();
|
||||
TRepository repository();
|
||||
|
||||
|
||||
+16
-8
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'loadable_save_context.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'loadable_save_context.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $LoadableSaveContextCopyWith<LoadableSaveContext> get copyWith => _$LoadableSave
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableSaveContext&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
|
||||
final _this = this as LoadableSaveContext;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableSaveContext&&(identical(other.timestamp, _this.timestamp) || other.timestamp == _this.timestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,timestamp);
|
||||
int get hashCode {
|
||||
final _this = this as LoadableSaveContext;
|
||||
return Object.hash(runtimeType,_this.timestamp);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableSaveContext(timestamp: $timestamp)';
|
||||
final _this = this as LoadableSaveContext;
|
||||
return 'LoadableSaveContext(timestamp: ${_this.timestamp})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$LoadableSaveContextCopyWithImpl<$Res>
|
||||
/// Create a copy of LoadableSaveContext
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? timestamp = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(LoadableSaveContext(
|
||||
timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
@@ -227,16 +233,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableSaveContext&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableSaveContext&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,timestamp);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,timestamp);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableSaveContext(timestamp: $timestamp)';
|
||||
return 'LoadableSaveContext(timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -177,9 +177,7 @@ class AppModule {
|
||||
for (final missing in Modules.values) {
|
||||
if (!seen.add(missing)) continue;
|
||||
var insertAt = 0;
|
||||
for (final predecessor in Modules.values.takeWhile(
|
||||
(m) => m != missing,
|
||||
)) {
|
||||
for (final predecessor in Modules.values.takeWhile((m) => m != missing)) {
|
||||
final pos = order.indexOf(predecessor);
|
||||
if (pos >= insertAt) insertAt = pos + 1;
|
||||
}
|
||||
@@ -231,33 +229,26 @@ class AppModule {
|
||||
static const int minBottomBarSlots = 3;
|
||||
static const int maxBottomBarSlots = 5;
|
||||
|
||||
static int resolveBottomBarSlotCount(BuildContext context) {
|
||||
static int _slotCount(BuildContext context, int available) {
|
||||
final settings = context.read<SettingsCubit>().val().modulesSettings;
|
||||
final available = modules(context).length;
|
||||
|
||||
int desired;
|
||||
if (settings.autoFillBottomBar) {
|
||||
// 4 content tabs so the bar shows 5 entries including "Mehr" — enough
|
||||
// for the Ticker module without pushing "Dateien" into the overflow.
|
||||
desired = 4;
|
||||
} else {
|
||||
desired = settings.fixedBottomBarSlots;
|
||||
}
|
||||
|
||||
desired = desired.clamp(minBottomBarSlots, maxBottomBarSlots);
|
||||
return desired.clamp(0, available);
|
||||
// 4 content tabs so the bar shows 5 entries including "Mehr" — enough
|
||||
// for the Ticker module without pushing "Dateien" into the overflow.
|
||||
final desired = settings.autoFillBottomBar
|
||||
? 4
|
||||
: settings.fixedBottomBarSlots;
|
||||
return desired
|
||||
.clamp(minBottomBarSlots, maxBottomBarSlots)
|
||||
.clamp(0, available);
|
||||
}
|
||||
|
||||
static List<AppModule> getBottomBarModules(BuildContext context) {
|
||||
final all = modules(context).values.toList();
|
||||
final slots = resolveBottomBarSlotCount(context);
|
||||
return all.take(slots).toList();
|
||||
return all.take(_slotCount(context, all.length)).toList();
|
||||
}
|
||||
|
||||
static List<AppModule> getOverhangModules(BuildContext context) {
|
||||
final all = modules(context).values.toList();
|
||||
final slots = resolveBottomBarSlotCount(context);
|
||||
return all.skip(slots).toList();
|
||||
return all.skip(_slotCount(context, all.length)).toList();
|
||||
}
|
||||
|
||||
Widget toListTile(
|
||||
@@ -290,10 +281,7 @@ class AppModule {
|
||||
: const Icon(Icons.arrow_right),
|
||||
);
|
||||
|
||||
PersistentTabConfig toBottomTab(
|
||||
BuildContext context, {
|
||||
Widget Function(IconData icon)? iconBuilder,
|
||||
}) => PersistentTabConfig(
|
||||
PersistentTabConfig toBottomTab(BuildContext context) => PersistentTabConfig(
|
||||
screen: Breaker(breaker: breakerArea, child: create()),
|
||||
item: ItemConfig(
|
||||
activeForegroundColor: Theme.of(context).primaryColor,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'breaker_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'breaker_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $BreakerStateCopyWith<BreakerState> get copyWith => _$BreakerStateCopyWithImpl<B
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BreakerState&&(identical(other.response, response) || other.response == response));
|
||||
final _this = this as BreakerState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BreakerState&&(identical(other.response, _this.response) || other.response == _this.response));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,response);
|
||||
int get hashCode {
|
||||
final _this = this as BreakerState;
|
||||
return Object.hash(runtimeType,_this.response);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BreakerState(response: $response)';
|
||||
final _this = this as BreakerState;
|
||||
return 'BreakerState(response: ${_this.response})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$BreakerStateCopyWithImpl<$Res>
|
||||
/// Create a copy of BreakerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? response = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(BreakerState(
|
||||
response: freezed == response ? _self.response : response // ignore: cast_nullable_to_non_nullable
|
||||
as GetBreakersResponse?,
|
||||
));
|
||||
@@ -227,16 +233,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BreakerState&&(identical(other.response, response) || other.response == response));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BreakerState&&(identical(other.response, response) || other.response == response));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,response);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,response);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BreakerState(response: $response)';
|
||||
return 'BreakerState(response: $response)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ class BreakerDataProvider {
|
||||
bool renew = true,
|
||||
void Function(Object)? onError,
|
||||
}) => resolveFromCache<GetBreakersResponse>(
|
||||
(onUpdate, onError) =>
|
||||
GetBreakersCache(renew: renew, onUpdate: onUpdate, onError: onError),
|
||||
onError: onError,
|
||||
operationName: 'getBreakers',
|
||||
);
|
||||
(onUpdate, onError) =>
|
||||
GetBreakersCache(renew: renew, onUpdate: onUpdate, onError: onError),
|
||||
onError: onError,
|
||||
operationName: 'getBreakers',
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'capabilities_state.dart';
|
||||
@@ -9,19 +9,14 @@ part of 'capabilities_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$CapabilitiesState {
|
||||
|
||||
bool get viewForeignTimetables; bool get pushNotifications;// Days into the past/future the timetable may be scrolled. Null = no
|
||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||
int? get timetablePastDays; int? get timetableFutureDays;// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||
String? get userType;// Whether a capability response (or a definitive failure) has been
|
||||
// observed at least once this session. Lets the UI distinguish "still
|
||||
// unknown" from "confirmed not allowed".
|
||||
bool get loaded;
|
||||
bool get viewForeignTimetables; bool get pushNotifications; int? get timetablePastDays; int? get timetableFutureDays; String? get userType; bool get loaded;
|
||||
/// Create a copy of CapabilitiesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -34,16 +29,21 @@ $CapabilitiesStateCopyWith<CapabilitiesState> get copyWith => _$CapabilitiesStat
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
final _this = this as CapabilitiesState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CapabilitiesState&&(identical(other.viewForeignTimetables, _this.viewForeignTimetables) || other.viewForeignTimetables == _this.viewForeignTimetables)&&(identical(other.pushNotifications, _this.pushNotifications) || other.pushNotifications == _this.pushNotifications)&&(identical(other.timetablePastDays, _this.timetablePastDays) || other.timetablePastDays == _this.timetablePastDays)&&(identical(other.timetableFutureDays, _this.timetableFutureDays) || other.timetableFutureDays == _this.timetableFutureDays)&&(identical(other.userType, _this.userType) || other.userType == _this.userType)&&(identical(other.loaded, _this.loaded) || other.loaded == _this.loaded));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,loaded);
|
||||
int get hashCode {
|
||||
final _this = this as CapabilitiesState;
|
||||
return Object.hash(runtimeType,_this.viewForeignTimetables,_this.pushNotifications,_this.timetablePastDays,_this.timetableFutureDays,_this.userType,_this.loaded);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, loaded: $loaded)';
|
||||
final _this = this as CapabilitiesState;
|
||||
return 'CapabilitiesState(viewForeignTimetables: ${_this.viewForeignTimetables}, pushNotifications: ${_this.pushNotifications}, timetablePastDays: ${_this.timetablePastDays}, timetableFutureDays: ${_this.timetableFutureDays}, userType: ${_this.userType}, loaded: ${_this.loaded})';
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class _$CapabilitiesStateCopyWithImpl<$Res>
|
||||
/// Create a copy of CapabilitiesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? userType = freezed,Object? loaded = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(CapabilitiesState(
|
||||
viewForeignTimetables: null == viewForeignTimetables ? _self.viewForeignTimetables : viewForeignTimetables // ignore: cast_nullable_to_non_nullable
|
||||
as bool,pushNotifications: null == pushNotifications ? _self.pushNotifications : pushNotifications // ignore: cast_nullable_to_non_nullable
|
||||
as bool,timetablePastDays: freezed == timetablePastDays ? _self.timetablePastDays : timetablePastDays // ignore: cast_nullable_to_non_nullable
|
||||
@@ -225,15 +225,9 @@ class _CapabilitiesState implements CapabilitiesState {
|
||||
|
||||
@override@JsonKey() final bool viewForeignTimetables;
|
||||
@override@JsonKey() final bool pushNotifications;
|
||||
// Days into the past/future the timetable may be scrolled. Null = no
|
||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||
@override final int? timetablePastDays;
|
||||
@override final int? timetableFutureDays;
|
||||
// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||
@override final String? userType;
|
||||
// Whether a capability response (or a definitive failure) has been
|
||||
// observed at least once this session. Lets the UI distinguish "still
|
||||
// unknown" from "confirmed not allowed".
|
||||
@override@JsonKey() final bool loaded;
|
||||
|
||||
/// Create a copy of CapabilitiesState
|
||||
@@ -249,16 +243,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,loaded);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,loaded);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, loaded: $loaded)';
|
||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, loaded: $loaded)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../../../../api/demo/demo_mode.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_history.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/long_poll_chat.dart';
|
||||
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../../api/marianumcloud/talk/set_read_marker/set_read_marker.dart';
|
||||
import '../../../../../api/marianumcloud/talk/set_read_marker/set_read_marker_params.dart';
|
||||
import '../../../../../utils/exponential_backoff.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../../chat_list/bloc/chat_list_bloc.dart';
|
||||
@@ -26,7 +27,6 @@ class ChatBloc
|
||||
// A loop parked in its request survives stop+start for the same token
|
||||
// (pause/resume); the generation retires it so only one loop polls.
|
||||
int _pollGeneration = 0;
|
||||
int _backoffMs = 0;
|
||||
int _lastKnownMessageId = 0;
|
||||
bool _appResumed = true;
|
||||
|
||||
@@ -68,8 +68,7 @@ class ChatBloc
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(ChatState state) {
|
||||
final response = state.chatResponse;
|
||||
if (response == null ||
|
||||
response.data.length <= _kMaxPersistedMessages) {
|
||||
if (response == null || response.data.length <= _kMaxPersistedMessages) {
|
||||
return state.toJson();
|
||||
}
|
||||
// Keep only the newest N; trimming drops older messages, so there is
|
||||
@@ -129,9 +128,6 @@ class ChatBloc
|
||||
add(Emit((s) => s.copyWith(referenceMessageId: messageId)));
|
||||
}
|
||||
|
||||
/// No-op when the bloc has already moved on to a different token: when
|
||||
/// popping a stacked chat (B over A), A's didPopNext runs setToken(A)
|
||||
/// before B's dispose fires.
|
||||
/// The chat view may still be popping when the sign-out resets this bloc,
|
||||
/// so leaveChat would find no token and the long-poll would keep running.
|
||||
@override
|
||||
@@ -141,6 +137,9 @@ class ChatBloc
|
||||
return super.reset();
|
||||
}
|
||||
|
||||
/// No-op when the bloc has already moved on to a different token: when
|
||||
/// popping a stacked chat (B over A), A's didPopNext runs setToken(A)
|
||||
/// before B's dispose fires.
|
||||
void leaveChat(String fromToken) {
|
||||
if ((innerState?.currentToken ?? '') != fromToken) return;
|
||||
_chatViewActive = false;
|
||||
@@ -264,7 +263,7 @@ class ChatBloc
|
||||
}
|
||||
|
||||
void _startLongPoll(String token) {
|
||||
if (!_appResumed) return;
|
||||
if (!_appResumed || DemoMode.active) return;
|
||||
// A load chain may finish after the user switched chats (A→B); without this
|
||||
// guard the stale chain hijacks the long-poll back to A and merges A's
|
||||
// messages into B's state while B never receives live updates.
|
||||
@@ -272,7 +271,6 @@ class ChatBloc
|
||||
if (_pollingToken == token) return;
|
||||
_stopLongPoll();
|
||||
_pollingToken = token;
|
||||
_backoffMs = 0;
|
||||
_lastKnownMessageId = _maxMessageId(innerState?.chatResponse);
|
||||
unawaited(_pollLoop(token, ++_pollGeneration));
|
||||
}
|
||||
@@ -280,11 +278,11 @@ class ChatBloc
|
||||
void _stopLongPoll() {
|
||||
_pollingToken = null;
|
||||
_pollGeneration++;
|
||||
_backoffMs = 0;
|
||||
}
|
||||
|
||||
Future<void> _pollLoop(String token, int generation) async {
|
||||
bool active() => generation == _pollGeneration && !isClosed;
|
||||
var failures = 0;
|
||||
while (active()) {
|
||||
try {
|
||||
final response = await LongPollChat(
|
||||
@@ -293,7 +291,7 @@ class ChatBloc
|
||||
).run();
|
||||
|
||||
if (!active()) return;
|
||||
_backoffMs = 0;
|
||||
failures = 0;
|
||||
|
||||
if (response == null) continue;
|
||||
|
||||
@@ -318,8 +316,13 @@ class ChatBloc
|
||||
} on Object catch (e) {
|
||||
if (!active()) return;
|
||||
log('LongPoll error for $token: $e');
|
||||
_backoffMs = _backoffMs == 0 ? 2000 : math.min(_backoffMs * 2, 30000);
|
||||
await Future.delayed(Duration(milliseconds: _backoffMs));
|
||||
await Future<void>.delayed(
|
||||
exponentialBackoff(
|
||||
++failures,
|
||||
base: const Duration(seconds: 2),
|
||||
max: const Duration(seconds: 30),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,14 +342,7 @@ class ChatBloc
|
||||
);
|
||||
return;
|
||||
}
|
||||
final byId = <int, GetChatResponseObject>{};
|
||||
for (final m in current.data) {
|
||||
byId[m.id] = m;
|
||||
}
|
||||
for (final m in incoming.data) {
|
||||
byId[m.id] = m;
|
||||
}
|
||||
final merged = GetChatResponse(byId.values.toSet())
|
||||
final merged = _mergeById(current, incoming, incomingWins: true)
|
||||
..headers = incoming.headers;
|
||||
add(DataGathered((s) => s.copyWith(chatResponse: merged)));
|
||||
}
|
||||
@@ -357,14 +353,7 @@ class ChatBloc
|
||||
void _applyOlderResponse(GetChatResponse older) {
|
||||
final current = innerState?.chatResponse;
|
||||
if (current == null) return;
|
||||
final byId = <int, GetChatResponseObject>{};
|
||||
for (final m in current.data) {
|
||||
byId[m.id] = m;
|
||||
}
|
||||
for (final m in older.data) {
|
||||
byId.putIfAbsent(m.id, () => m);
|
||||
}
|
||||
final merged = GetChatResponse(byId.values.toSet())
|
||||
final merged = _mergeById(current, older, incomingWins: false)
|
||||
..headers = current.headers;
|
||||
add(
|
||||
DataGathered(
|
||||
@@ -377,6 +366,22 @@ class ChatBloc
|
||||
);
|
||||
}
|
||||
|
||||
GetChatResponse _mergeById(
|
||||
GetChatResponse current,
|
||||
GetChatResponse incoming, {
|
||||
required bool incomingWins,
|
||||
}) {
|
||||
final byId = {for (final m in current.data) m.id: m};
|
||||
for (final m in incoming.data) {
|
||||
if (incomingWins) {
|
||||
byId[m.id] = m;
|
||||
} else {
|
||||
byId.putIfAbsent(m.id, () => m);
|
||||
}
|
||||
}
|
||||
return GetChatResponse(byId.values.toSet());
|
||||
}
|
||||
|
||||
int _maxMessageId(GetChatResponse? response) {
|
||||
if (response == null) return 0;
|
||||
var max = 0;
|
||||
@@ -386,8 +391,7 @@ class ChatBloc
|
||||
return max;
|
||||
}
|
||||
|
||||
int _minMessageId(GetChatResponse? response) {
|
||||
if (response == null) return 0;
|
||||
int _minMessageId(GetChatResponse response) {
|
||||
var min = 0;
|
||||
for (final m in response.data) {
|
||||
if (m.id <= 0) continue; // skip dummies
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'chat_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'chat_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $ChatStateCopyWith<ChatState> get copyWith => _$ChatStateCopyWithImpl<ChatState>
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatState&&(identical(other.currentToken, currentToken) || other.currentToken == currentToken)&&(identical(other.chatResponse, chatResponse) || other.chatResponse == chatResponse)&&(identical(other.referenceMessageId, referenceMessageId) || other.referenceMessageId == referenceMessageId)&&(identical(other.hasMoreOld, hasMoreOld) || other.hasMoreOld == hasMoreOld)&&(identical(other.isLoadingOlder, isLoadingOlder) || other.isLoadingOlder == isLoadingOlder));
|
||||
final _this = this as ChatState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatState&&(identical(other.currentToken, _this.currentToken) || other.currentToken == _this.currentToken)&&(identical(other.chatResponse, _this.chatResponse) || other.chatResponse == _this.chatResponse)&&(identical(other.referenceMessageId, _this.referenceMessageId) || other.referenceMessageId == _this.referenceMessageId)&&(identical(other.hasMoreOld, _this.hasMoreOld) || other.hasMoreOld == _this.hasMoreOld)&&(identical(other.isLoadingOlder, _this.isLoadingOlder) || other.isLoadingOlder == _this.isLoadingOlder));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,currentToken,chatResponse,referenceMessageId,hasMoreOld,isLoadingOlder);
|
||||
int get hashCode {
|
||||
final _this = this as ChatState;
|
||||
return Object.hash(runtimeType,_this.currentToken,_this.chatResponse,_this.referenceMessageId,_this.hasMoreOld,_this.isLoadingOlder);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatState(currentToken: $currentToken, chatResponse: $chatResponse, referenceMessageId: $referenceMessageId, hasMoreOld: $hasMoreOld, isLoadingOlder: $isLoadingOlder)';
|
||||
final _this = this as ChatState;
|
||||
return 'ChatState(currentToken: ${_this.currentToken}, chatResponse: ${_this.chatResponse}, referenceMessageId: ${_this.referenceMessageId}, hasMoreOld: ${_this.hasMoreOld}, isLoadingOlder: ${_this.isLoadingOlder})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$ChatStateCopyWithImpl<$Res>
|
||||
/// Create a copy of ChatState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? currentToken = null,Object? chatResponse = freezed,Object? referenceMessageId = freezed,Object? hasMoreOld = null,Object? isLoadingOlder = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(ChatState(
|
||||
currentToken: null == currentToken ? _self.currentToken : currentToken // ignore: cast_nullable_to_non_nullable
|
||||
as String,chatResponse: freezed == chatResponse ? _self.chatResponse : chatResponse // ignore: cast_nullable_to_non_nullable
|
||||
as GetChatResponse?,referenceMessageId: freezed == referenceMessageId ? _self.referenceMessageId : referenceMessageId // ignore: cast_nullable_to_non_nullable
|
||||
@@ -235,16 +241,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatState&&(identical(other.currentToken, currentToken) || other.currentToken == currentToken)&&(identical(other.chatResponse, chatResponse) || other.chatResponse == chatResponse)&&(identical(other.referenceMessageId, referenceMessageId) || other.referenceMessageId == referenceMessageId)&&(identical(other.hasMoreOld, hasMoreOld) || other.hasMoreOld == hasMoreOld)&&(identical(other.isLoadingOlder, isLoadingOlder) || other.isLoadingOlder == isLoadingOlder));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatState&&(identical(other.currentToken, currentToken) || other.currentToken == currentToken)&&(identical(other.chatResponse, chatResponse) || other.chatResponse == chatResponse)&&(identical(other.referenceMessageId, referenceMessageId) || other.referenceMessageId == referenceMessageId)&&(identical(other.hasMoreOld, hasMoreOld) || other.hasMoreOld == hasMoreOld)&&(identical(other.isLoadingOlder, isLoadingOlder) || other.isLoadingOlder == isLoadingOlder));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,currentToken,chatResponse,referenceMessageId,hasMoreOld,isLoadingOlder);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,currentToken,chatResponse,referenceMessageId,hasMoreOld,isLoadingOlder);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatState(currentToken: $currentToken, chatResponse: $chatResponse, referenceMessageId: $referenceMessageId, hasMoreOld: $hasMoreOld, isLoadingOlder: $isLoadingOlder)';
|
||||
return 'ChatState(currentToken: $currentToken, chatResponse: $chatResponse, referenceMessageId: $referenceMessageId, hasMoreOld: $hasMoreOld, isLoadingOlder: $isLoadingOlder)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'chat_list_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'chat_list_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $ChatListStateCopyWith<ChatListState> get copyWith => _$ChatListStateCopyWithImp
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatListState&&(identical(other.rooms, rooms) || other.rooms == rooms));
|
||||
final _this = this as ChatListState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ChatListState&&(identical(other.rooms, _this.rooms) || other.rooms == _this.rooms));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,rooms);
|
||||
int get hashCode {
|
||||
final _this = this as ChatListState;
|
||||
return Object.hash(runtimeType,_this.rooms);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatListState(rooms: $rooms)';
|
||||
final _this = this as ChatListState;
|
||||
return 'ChatListState(rooms: ${_this.rooms})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$ChatListStateCopyWithImpl<$Res>
|
||||
/// Create a copy of ChatListState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? rooms = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(ChatListState(
|
||||
rooms: freezed == rooms ? _self.rooms : rooms // ignore: cast_nullable_to_non_nullable
|
||||
as GetRoomResponse?,
|
||||
));
|
||||
@@ -227,16 +233,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatListState&&(identical(other.rooms, rooms) || other.rooms == rooms));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChatListState&&(identical(other.rooms, rooms) || other.rooms == rooms));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,rooms);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,rooms);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChatListState(rooms: $rooms)';
|
||||
return 'ChatListState(rooms: $rooms)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,18 +28,16 @@ class FilesBloc
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(FilesState state) => null;
|
||||
|
||||
List<String> get _currentPath => innerState?.currentPath ?? initialPath;
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final path = innerState?.currentPath ?? initialPath;
|
||||
await _query(path);
|
||||
}
|
||||
Future<void> gatherData() => _query(_currentPath);
|
||||
|
||||
Future<void> refresh() async {
|
||||
add(RefetchStarted<FilesState>());
|
||||
final path = innerState?.currentPath ?? initialPath;
|
||||
// Explicit user action — bypass the cache TTL so the root listing also
|
||||
// refetches even though it is otherwise cached for a day.
|
||||
await _query(path, renew: true);
|
||||
await _query(_currentPath, renew: true);
|
||||
}
|
||||
|
||||
/// Pull-to-refresh and error-screen retry route through here. Unlike the
|
||||
@@ -57,13 +55,15 @@ class FilesBloc
|
||||
}
|
||||
|
||||
Future<void> createFolder(String name) async {
|
||||
final path = innerState?.currentPath ?? initialPath;
|
||||
await repo.data.createFolder('${path.join('/')}/$name');
|
||||
await repo.data.createFolder('${_currentPath.join('/')}/$name');
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Future<void> _query(List<String> path, {bool renew = false}) async {
|
||||
final pathString = path.isEmpty ? '/' : path.join('/');
|
||||
// The listing contains the folder itself; only its children are shown.
|
||||
void dropSelf(ListFilesResponse listing) => listing.files.removeWhere(
|
||||
(file) => file.name.isEmpty || file.name == path.lastOrNull,
|
||||
);
|
||||
|
||||
// Drop late results when [setPath] has navigated elsewhere or when the
|
||||
// bloc has been disposed (e.g. share-flow picker closed mid-fetch). Both
|
||||
@@ -80,16 +80,14 @@ class FilesBloc
|
||||
ListFilesResponse? listing;
|
||||
try {
|
||||
listing = await repo.data.listFiles(
|
||||
pathString,
|
||||
path.isEmpty ? '/' : path.join('/'),
|
||||
renew: renew,
|
||||
onCacheData: (cached) {
|
||||
if (isStale()) return;
|
||||
// Cached payload arrives before the network call settles. Surface it
|
||||
// immediately via Emit so the listing is visible while isLoading
|
||||
// stays true and the top loading bar keeps spinning.
|
||||
cached.files.removeWhere(
|
||||
(file) => file.name.isEmpty || file.name == path.lastOrNull,
|
||||
);
|
||||
dropSelf(cached);
|
||||
add(Emit((s) => s.copyWith(listing: cached)));
|
||||
},
|
||||
onRetry: (next, max) {
|
||||
@@ -105,9 +103,7 @@ class FilesBloc
|
||||
if (isStale()) return;
|
||||
|
||||
if (listing != null) {
|
||||
listing.files.removeWhere(
|
||||
(file) => file.name.isEmpty || file.name == path.lastOrNull,
|
||||
);
|
||||
dropSelf(listing);
|
||||
add(DataGathered((s) => s.copyWith(listing: listing)));
|
||||
}
|
||||
if (capturedError != null) addLoadingError(capturedError!);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'files_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'files_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $FilesStateCopyWith<FilesState> get copyWith => _$FilesStateCopyWithImpl<FilesSt
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is FilesState&&const DeepCollectionEquality().equals(other.currentPath, currentPath)&&(identical(other.listing, listing) || other.listing == listing));
|
||||
final _this = this as FilesState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is FilesState&&const DeepCollectionEquality().equals(other.currentPath, _this.currentPath)&&(identical(other.listing, _this.listing) || other.listing == _this.listing));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(currentPath),listing);
|
||||
int get hashCode {
|
||||
final _this = this as FilesState;
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.currentPath),_this.listing);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FilesState(currentPath: $currentPath, listing: $listing)';
|
||||
final _this = this as FilesState;
|
||||
return 'FilesState(currentPath: ${_this.currentPath}, listing: ${_this.listing})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$FilesStateCopyWithImpl<$Res>
|
||||
/// Create a copy of FilesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? currentPath = null,Object? listing = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(FilesState(
|
||||
currentPath: null == currentPath ? _self.currentPath : currentPath // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,listing: freezed == listing ? _self.listing : listing // ignore: cast_nullable_to_non_nullable
|
||||
as ListFilesResponse?,
|
||||
@@ -210,7 +216,7 @@ return $default(_that.currentPath,_that.listing);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _FilesState implements FilesState {
|
||||
const _FilesState({final List<String> currentPath = const <String>[], this.listing}): _currentPath = currentPath;
|
||||
const _FilesState({ List<String> currentPath = const <String>[], this.listing}): _currentPath = currentPath;
|
||||
factory _FilesState.fromJson(Map<String, dynamic> json) => _$FilesStateFromJson(json);
|
||||
|
||||
final List<String> _currentPath;
|
||||
@@ -235,16 +241,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _FilesState&&const DeepCollectionEquality().equals(other._currentPath, _currentPath)&&(identical(other.listing, listing) || other.listing == listing));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _FilesState&&const DeepCollectionEquality().equals(other.currentPath, _currentPath)&&(identical(other.listing, listing) || other.listing == listing));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_currentPath),listing);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_currentPath),listing);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FilesState(currentPath: $currentPath, listing: $listing)';
|
||||
return 'FilesState(currentPath: $currentPath, listing: $listing)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,5 @@ import '../bloc/files_state.dart';
|
||||
import '../data_provider/files_data_provider.dart';
|
||||
|
||||
class FilesRepository extends Repository<FilesState> {
|
||||
final FilesDataProvider _provider;
|
||||
|
||||
FilesRepository([FilesDataProvider? provider])
|
||||
: _provider = provider ?? FilesDataProvider();
|
||||
|
||||
FilesDataProvider get data => _provider;
|
||||
final FilesDataProvider data = FilesDataProvider();
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../../extensions/date_time.dart';
|
||||
import '../../../infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../../timetable/bloc/timetable_event.dart';
|
||||
import '../../timetable/bloc/timetable_state.dart';
|
||||
import '../../timetable/bloc/timetable_week_loader.dart';
|
||||
import '../../timetable/data_provider/timetable_data_provider.dart';
|
||||
import '../repository/foreign_timetable_repository.dart';
|
||||
|
||||
/// Drives a foreign element's timetable. Mirrors `TimetableBloc`'s week-loading
|
||||
/// Drives a foreign element's timetable. Shares `TimetableBloc`'s week loading
|
||||
/// and navigation but loads weeks from the element endpoint, carries no custom
|
||||
/// events, and does not persist (page-scoped, recreated per element). Reuses
|
||||
/// [TimetableState] verbatim so the render pipeline is unchanged; `customEvents`
|
||||
@@ -21,34 +19,25 @@ class ForeignTimetableBloc
|
||||
TimetableEvent,
|
||||
TimetableState,
|
||||
ForeignTimetableRepository
|
||||
> {
|
||||
|
||||
>
|
||||
with TimetableWeekLoader<ForeignTimetableRepository> {
|
||||
final TimetableElementType type;
|
||||
// Named `elementId` rather than `id` to avoid shadowing HydratedMixin's
|
||||
// `String get id` (the storage key), which a plain `int id` would illegally
|
||||
// override.
|
||||
final int elementId;
|
||||
final String title;
|
||||
|
||||
DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
ForeignTimetableBloc({
|
||||
required this.type,
|
||||
required this.elementId,
|
||||
required this.title,
|
||||
});
|
||||
ForeignTimetableBloc({required this.type, required this.elementId});
|
||||
|
||||
@override
|
||||
ForeignTimetableRepository repository() => ForeignTimetableRepository();
|
||||
|
||||
@override
|
||||
TimetableState fromNothing() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
return TimetableState(
|
||||
startDate: reference.mondayOfWeek,
|
||||
endDate: _endOfWeek(reference),
|
||||
);
|
||||
}
|
||||
TimetableDataProvider get referenceData => repo.data.referenceData;
|
||||
|
||||
@override
|
||||
Future<TimetableGetWeekResponse> fetchWeek(DateTime start, DateTime end) =>
|
||||
repo.data.getElementWeek(type, elementId, start, end);
|
||||
|
||||
// Persistence disabled: page-scoped and element-specific, nothing worth
|
||||
// restoring. toJson returns null so nothing is written; fromJson starts fresh.
|
||||
@@ -72,116 +61,5 @@ class ForeignTimetableBloc
|
||||
Map<String, dynamic>? toStorage(TimetableState state) => null;
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final initial = innerState ?? fromNothing();
|
||||
|
||||
Object? firstError;
|
||||
void recordError(Object e) {
|
||||
firstError ??= e;
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_loadCurrentWeek(initial.startDate, initial.endDate, onError: recordError),
|
||||
_loadStaticReferenceData(onError: recordError),
|
||||
]);
|
||||
|
||||
if (firstError != null) throw firstError!;
|
||||
|
||||
add(DataGathered((s) => s));
|
||||
_prefetchAdjacentWeeks(initial.startDate, initial.endDate);
|
||||
}
|
||||
|
||||
void changeWeek(DateTime startDate, DateTime endDate) {
|
||||
final current = innerState ?? fromNothing();
|
||||
if (current.startDate == startDate && current.endDate == endDate) return;
|
||||
add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate)));
|
||||
_loadCurrentWeek(startDate, endDate);
|
||||
_prefetchAdjacentWeeks(startDate, endDate);
|
||||
}
|
||||
|
||||
void resetWeek() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
changeWeek(reference.mondayOfWeek, _endOfWeek(reference));
|
||||
}
|
||||
|
||||
void refresh() => fetch();
|
||||
|
||||
Future<void> _loadCurrentWeek(
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
final requestStart = DateTime.now();
|
||||
_lastWeekRequestStart = requestStart;
|
||||
try {
|
||||
final week = await repo.data.getElementWeek(
|
||||
type,
|
||||
elementId,
|
||||
startDate,
|
||||
endDate,
|
||||
onError: onError,
|
||||
);
|
||||
if (_lastWeekRequestStart.isAfter(requestStart)) return;
|
||||
_writeWeekToCache(startDate, week);
|
||||
} catch (e) {
|
||||
log('getElementWeek error for $startDate–$endDate: $e');
|
||||
onError?.call(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadStaticReferenceData({
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
try {
|
||||
final (rooms, subjects, schoolHolidays, schoolyear) = await (
|
||||
repo.data.getRooms(onError: onError),
|
||||
repo.data.getSubjects(onError: onError),
|
||||
repo.data.getSchoolHolidays(onError: onError),
|
||||
repo.data.getCurrentSchoolyear(onError: onError),
|
||||
).wait;
|
||||
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.withReferenceData(
|
||||
rooms: rooms,
|
||||
subjects: subjects,
|
||||
schoolHolidays: schoolHolidays,
|
||||
schoolyear: schoolyear,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
}
|
||||
|
||||
try {
|
||||
final timegrid = await repo.data.getTimegrid();
|
||||
add(Emit((s) => s.withReferenceData(timegrid: timegrid)));
|
||||
} catch (_) {
|
||||
// Timegrid load failure falls back to a hardcoded schedule in the UI.
|
||||
}
|
||||
}
|
||||
|
||||
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
|
||||
_prefetchWeek(start.subtractDays(7), end.subtractDays(7));
|
||||
_prefetchWeek(start.addDays(7), end.addDays(7));
|
||||
}
|
||||
|
||||
void _prefetchWeek(DateTime start, DateTime end) {
|
||||
repo.data
|
||||
.getElementWeek(type, elementId, start, end)
|
||||
.then((week) => _writeWeekToCache(start, week))
|
||||
.catchError((_) {});
|
||||
}
|
||||
|
||||
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
|
||||
add(Emit((s) => s.withFetchedWeek(weekStart, week)));
|
||||
}
|
||||
|
||||
static DateTime _endOfWeek(DateTime reference) {
|
||||
final friday = reference.addDays(
|
||||
DateTime.daysPerWeek - reference.weekday - 2,
|
||||
);
|
||||
return DateTime(friday.year, friday.month, friday.day);
|
||||
}
|
||||
Future<void> gatherData() => gatherWeek();
|
||||
}
|
||||
+12
-49
@@ -1,64 +1,27 @@
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_get_element_week.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_schoolyear/timetable_get_schoolyear_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../timetable/data_provider/timetable_data_provider.dart';
|
||||
|
||||
/// Data access for a foreign element's timetable. The week comes from the
|
||||
/// element-specific endpoint; all reference data (rooms/subjects/holidays/
|
||||
/// school year/timegrid) is school-wide, so it delegates to the existing
|
||||
/// [TimetableDataProvider] (which caches it). Custom events are intentionally
|
||||
/// absent — they are user-private.
|
||||
/// school year/timegrid) is school-wide and comes from [referenceData].
|
||||
/// Custom events are intentionally absent — they are user-private.
|
||||
class ForeignTimetableDataProvider {
|
||||
final TimetableDataProvider _base;
|
||||
final TimetableDataProvider referenceData;
|
||||
|
||||
ForeignTimetableDataProvider([TimetableDataProvider? base])
|
||||
: _base = base ?? TimetableDataProvider();
|
||||
ForeignTimetableDataProvider([TimetableDataProvider? referenceData])
|
||||
: referenceData = referenceData ?? TimetableDataProvider();
|
||||
|
||||
Future<TimetableGetWeekResponse> getElementWeek(
|
||||
TimetableElementType type,
|
||||
int id,
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetElementWeek().run(
|
||||
type: type,
|
||||
id: id,
|
||||
from: startDate,
|
||||
until: endDate,
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<TimetableGetRoomsResponse> getRooms({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getRooms(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetSubjectsResponse> getSubjects({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getSubjects(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetHolidaysResponse> getSchoolHolidays({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getSchoolHolidays(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetSchoolyearResponse> getCurrentSchoolyear({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getCurrentSchoolyear(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetTimegridResponse> getTimegrid({bool renew = false}) =>
|
||||
_base.getTimegrid(renew: renew);
|
||||
DateTime endDate,
|
||||
) async => TimetableGetElementWeek().run(
|
||||
type: type,
|
||||
id: id,
|
||||
from: startDate,
|
||||
until: endDate,
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'grade_averages_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'grade_averages_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $GradeAveragesStateCopyWith<GradeAveragesState> get copyWith => _$GradeAveragesS
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is GradeAveragesState&&(identical(other.gradingSystem, gradingSystem) || other.gradingSystem == gradingSystem)&&const DeepCollectionEquality().equals(other.grades, grades));
|
||||
final _this = this as GradeAveragesState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is GradeAveragesState&&(identical(other.gradingSystem, _this.gradingSystem) || other.gradingSystem == _this.gradingSystem)&&const DeepCollectionEquality().equals(other.grades, _this.grades));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,gradingSystem,const DeepCollectionEquality().hash(grades));
|
||||
int get hashCode {
|
||||
final _this = this as GradeAveragesState;
|
||||
return Object.hash(runtimeType,_this.gradingSystem,const DeepCollectionEquality().hash(_this.grades));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GradeAveragesState(gradingSystem: $gradingSystem, grades: $grades)';
|
||||
final _this = this as GradeAveragesState;
|
||||
return 'GradeAveragesState(gradingSystem: ${_this.gradingSystem}, grades: ${_this.grades})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$GradeAveragesStateCopyWithImpl<$Res>
|
||||
/// Create a copy of GradeAveragesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? gradingSystem = null,Object? grades = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(GradeAveragesState(
|
||||
gradingSystem: null == gradingSystem ? _self.gradingSystem : gradingSystem // ignore: cast_nullable_to_non_nullable
|
||||
as GradeAveragesGradingSystem,grades: null == grades ? _self.grades : grades // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,
|
||||
@@ -210,7 +216,7 @@ return $default(_that.gradingSystem,_that.grades);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _GradeAveragesState implements GradeAveragesState {
|
||||
const _GradeAveragesState({required this.gradingSystem, required final List<int> grades}): _grades = grades;
|
||||
const _GradeAveragesState({required this.gradingSystem, required List<int> grades}): _grades = grades;
|
||||
factory _GradeAveragesState.fromJson(Map<String, dynamic> json) => _$GradeAveragesStateFromJson(json);
|
||||
|
||||
@override final GradeAveragesGradingSystem gradingSystem;
|
||||
@@ -235,16 +241,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _GradeAveragesState&&(identical(other.gradingSystem, gradingSystem) || other.gradingSystem == gradingSystem)&&const DeepCollectionEquality().equals(other._grades, _grades));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _GradeAveragesState&&(identical(other.gradingSystem, gradingSystem) || other.gradingSystem == gradingSystem)&&const DeepCollectionEquality().equals(other.grades, _grades));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,gradingSystem,const DeepCollectionEquality().hash(_grades));
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,gradingSystem,const DeepCollectionEquality().hash(_grades));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GradeAveragesState(gradingSystem: $gradingSystem, grades: $grades)';
|
||||
return 'GradeAveragesState(gradingSystem: $gradingSystem, grades: $grades)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ class HolidaysBloc
|
||||
extends
|
||||
LoadableHydratedBloc<HolidaysEvent, HolidaysState, HolidaysRepository> {
|
||||
HolidaysBloc() {
|
||||
on<SetPastHolidaysVisible>((event, emit) {
|
||||
add(
|
||||
on<SetPastHolidaysVisible>(
|
||||
(event, emit) => add(
|
||||
Emit(
|
||||
(state) => state.copyWith(showPastHolidays: event.shouldBeVisible),
|
||||
),
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
|
||||
on<DisclaimerDismissed>(
|
||||
(event, emit) =>
|
||||
@@ -25,15 +25,13 @@ class HolidaysBloc
|
||||
|
||||
bool showPastHolidays() => innerState?.showPastHolidays ?? false;
|
||||
bool showDisclaimerOnEntry() => innerState?.showDisclaimer ?? false;
|
||||
List<McHoliday>? getHolidays() =>
|
||||
innerState?.holidays
|
||||
.where(
|
||||
(element) =>
|
||||
showPastHolidays() ||
|
||||
element.endDate.isAfter(DateTime.now()),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
List<McHoliday> getHolidays() {
|
||||
final now = DateTime.now();
|
||||
return innerState?.holidays
|
||||
.where((h) => showPastHolidays() || h.endDate.isAfter(now))
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
HolidaysState fromNothing() => const HolidaysState(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'holidays_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'holidays_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -27,23 +28,29 @@ $HolidaysStateCopyWith<HolidaysState> get copyWith => _$HolidaysStateCopyWithImp
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
final _this = this as HolidaysState;
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'HolidaysState'))
|
||||
..add(DiagnosticsProperty('showPastHolidays', showPastHolidays))..add(DiagnosticsProperty('showDisclaimer', showDisclaimer))..add(DiagnosticsProperty('holidays', holidays));
|
||||
..add(DiagnosticsProperty('showPastHolidays', _this.showPastHolidays))..add(DiagnosticsProperty('showDisclaimer', _this.showDisclaimer))..add(DiagnosticsProperty('holidays', _this.holidays));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is HolidaysState&&(identical(other.showPastHolidays, showPastHolidays) || other.showPastHolidays == showPastHolidays)&&(identical(other.showDisclaimer, showDisclaimer) || other.showDisclaimer == showDisclaimer)&&const DeepCollectionEquality().equals(other.holidays, holidays));
|
||||
final _this = this as HolidaysState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is HolidaysState&&(identical(other.showPastHolidays, _this.showPastHolidays) || other.showPastHolidays == _this.showPastHolidays)&&(identical(other.showDisclaimer, _this.showDisclaimer) || other.showDisclaimer == _this.showDisclaimer)&&const DeepCollectionEquality().equals(other.holidays, _this.holidays));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,showPastHolidays,showDisclaimer,const DeepCollectionEquality().hash(holidays));
|
||||
int get hashCode {
|
||||
final _this = this as HolidaysState;
|
||||
return Object.hash(runtimeType,_this.showPastHolidays,_this.showDisclaimer,const DeepCollectionEquality().hash(_this.holidays));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'HolidaysState(showPastHolidays: $showPastHolidays, showDisclaimer: $showDisclaimer, holidays: $holidays)';
|
||||
final _this = this as HolidaysState;
|
||||
return 'HolidaysState(showPastHolidays: ${_this.showPastHolidays}, showDisclaimer: ${_this.showDisclaimer}, holidays: ${_this.holidays})';
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +79,7 @@ class _$HolidaysStateCopyWithImpl<$Res>
|
||||
/// Create a copy of HolidaysState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? showPastHolidays = null,Object? showDisclaimer = null,Object? holidays = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(HolidaysState(
|
||||
showPastHolidays: null == showPastHolidays ? _self.showPastHolidays : showPastHolidays // ignore: cast_nullable_to_non_nullable
|
||||
as bool,showDisclaimer: null == showDisclaimer ? _self.showDisclaimer : showDisclaimer // ignore: cast_nullable_to_non_nullable
|
||||
as bool,holidays: null == holidays ? _self.holidays : holidays // ignore: cast_nullable_to_non_nullable
|
||||
@@ -217,7 +224,7 @@ return $default(_that.showPastHolidays,_that.showDisclaimer,_that.holidays);case
|
||||
@JsonSerializable()
|
||||
|
||||
class _HolidaysState with DiagnosticableTreeMixin implements HolidaysState {
|
||||
const _HolidaysState({required this.showPastHolidays, required this.showDisclaimer, required final List<McHoliday> holidays}): _holidays = holidays;
|
||||
const _HolidaysState({required this.showPastHolidays, required this.showDisclaimer, required List<McHoliday> holidays}): _holidays = holidays;
|
||||
factory _HolidaysState.fromJson(Map<String, dynamic> json) => _$HolidaysStateFromJson(json);
|
||||
|
||||
@override final bool showPastHolidays;
|
||||
@@ -242,23 +249,25 @@ Map<String, dynamic> toJson() {
|
||||
}
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'HolidaysState'))
|
||||
..add(DiagnosticsProperty('showPastHolidays', showPastHolidays))..add(DiagnosticsProperty('showDisclaimer', showDisclaimer))..add(DiagnosticsProperty('holidays', holidays));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HolidaysState&&(identical(other.showPastHolidays, showPastHolidays) || other.showPastHolidays == showPastHolidays)&&(identical(other.showDisclaimer, showDisclaimer) || other.showDisclaimer == showDisclaimer)&&const DeepCollectionEquality().equals(other._holidays, _holidays));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HolidaysState&&(identical(other.showPastHolidays, showPastHolidays) || other.showPastHolidays == showPastHolidays)&&(identical(other.showDisclaimer, showDisclaimer) || other.showDisclaimer == showDisclaimer)&&const DeepCollectionEquality().equals(other.holidays, _holidays));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,showPastHolidays,showDisclaimer,const DeepCollectionEquality().hash(_holidays));
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,showPastHolidays,showDisclaimer,const DeepCollectionEquality().hash(_holidays));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'HolidaysState(showPastHolidays: $showPastHolidays, showDisclaimer: $showDisclaimer, holidays: $holidays)';
|
||||
return 'HolidaysState(showPastHolidays: $showPastHolidays, showDisclaimer: $showDisclaimer, holidays: $holidays)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,20 +12,22 @@ class MarianumDatesBloc
|
||||
MarianumDatesRepository
|
||||
> {
|
||||
MarianumDatesBloc() {
|
||||
on<SetPastEventsVisible>((event, emit) {
|
||||
add(
|
||||
on<SetPastEventsVisible>(
|
||||
(event, emit) => add(
|
||||
Emit((state) => state.copyWith(showPastEvents: event.shouldBeVisible)),
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool showPastEvents() => innerState?.showPastEvents ?? false;
|
||||
|
||||
List<MarianumDate>? getEvents() =>
|
||||
innerState?.events
|
||||
.where((e) => showPastEvents() || e.end.isAfter(DateTime.now()))
|
||||
.toList() ??
|
||||
[];
|
||||
List<MarianumDate> getEvents() {
|
||||
final now = DateTime.now();
|
||||
return innerState?.events
|
||||
.where((e) => showPastEvents() || e.end.isAfter(now))
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
MarianumDatesState fromNothing() =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'marianum_dates_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'marianum_dates_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -27,23 +28,29 @@ $MarianumDatesStateCopyWith<MarianumDatesState> get copyWith => _$MarianumDatesS
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
final _this = this as MarianumDatesState;
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'MarianumDatesState'))
|
||||
..add(DiagnosticsProperty('showPastEvents', showPastEvents))..add(DiagnosticsProperty('events', events));
|
||||
..add(DiagnosticsProperty('showPastEvents', _this.showPastEvents))..add(DiagnosticsProperty('events', _this.events));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumDatesState&&(identical(other.showPastEvents, showPastEvents) || other.showPastEvents == showPastEvents)&&const DeepCollectionEquality().equals(other.events, events));
|
||||
final _this = this as MarianumDatesState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumDatesState&&(identical(other.showPastEvents, _this.showPastEvents) || other.showPastEvents == _this.showPastEvents)&&const DeepCollectionEquality().equals(other.events, _this.events));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,showPastEvents,const DeepCollectionEquality().hash(events));
|
||||
int get hashCode {
|
||||
final _this = this as MarianumDatesState;
|
||||
return Object.hash(runtimeType,_this.showPastEvents,const DeepCollectionEquality().hash(_this.events));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'MarianumDatesState(showPastEvents: $showPastEvents, events: $events)';
|
||||
final _this = this as MarianumDatesState;
|
||||
return 'MarianumDatesState(showPastEvents: ${_this.showPastEvents}, events: ${_this.events})';
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +79,7 @@ class _$MarianumDatesStateCopyWithImpl<$Res>
|
||||
/// Create a copy of MarianumDatesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? showPastEvents = null,Object? events = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(MarianumDatesState(
|
||||
showPastEvents: null == showPastEvents ? _self.showPastEvents : showPastEvents // ignore: cast_nullable_to_non_nullable
|
||||
as bool,events: null == events ? _self.events : events // ignore: cast_nullable_to_non_nullable
|
||||
as List<MarianumDate>,
|
||||
@@ -216,7 +223,7 @@ return $default(_that.showPastEvents,_that.events);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _MarianumDatesState with DiagnosticableTreeMixin implements MarianumDatesState {
|
||||
const _MarianumDatesState({required this.showPastEvents, required final List<MarianumDate> events}): _events = events;
|
||||
const _MarianumDatesState({required this.showPastEvents, required List<MarianumDate> events}): _events = events;
|
||||
factory _MarianumDatesState.fromJson(Map<String, dynamic> json) => _$MarianumDatesStateFromJson(json);
|
||||
|
||||
@override final bool showPastEvents;
|
||||
@@ -240,23 +247,25 @@ Map<String, dynamic> toJson() {
|
||||
}
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'MarianumDatesState'))
|
||||
..add(DiagnosticsProperty('showPastEvents', showPastEvents))..add(DiagnosticsProperty('events', events));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumDatesState&&(identical(other.showPastEvents, showPastEvents) || other.showPastEvents == showPastEvents)&&const DeepCollectionEquality().equals(other._events, _events));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumDatesState&&(identical(other.showPastEvents, showPastEvents) || other.showPastEvents == showPastEvents)&&const DeepCollectionEquality().equals(other.events, _events));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,showPastEvents,const DeepCollectionEquality().hash(_events));
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,showPastEvents,const DeepCollectionEquality().hash(_events));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'MarianumDatesState(showPastEvents: $showPastEvents, events: $events)';
|
||||
return 'MarianumDatesState(showPastEvents: $showPastEvents, events: $events)';
|
||||
}
|
||||
|
||||
|
||||
@@ -311,23 +320,29 @@ $MarianumDateCopyWith<MarianumDate> get copyWith => _$MarianumDateCopyWithImpl<M
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
final _this = this as MarianumDate;
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'MarianumDate'))
|
||||
..add(DiagnosticsProperty('uid', uid))..add(DiagnosticsProperty('title', title))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('start', start))..add(DiagnosticsProperty('end', end))..add(DiagnosticsProperty('isAllDay', isAllDay));
|
||||
..add(DiagnosticsProperty('uid', _this.uid))..add(DiagnosticsProperty('title', _this.title))..add(DiagnosticsProperty('description', _this.description))..add(DiagnosticsProperty('start', _this.start))..add(DiagnosticsProperty('end', _this.end))..add(DiagnosticsProperty('isAllDay', _this.isAllDay));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumDate&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.start, start) || other.start == start)&&(identical(other.end, end) || other.end == end)&&(identical(other.isAllDay, isAllDay) || other.isAllDay == isAllDay));
|
||||
final _this = this as MarianumDate;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumDate&&(identical(other.uid, _this.uid) || other.uid == _this.uid)&&(identical(other.title, _this.title) || other.title == _this.title)&&(identical(other.description, _this.description) || other.description == _this.description)&&(identical(other.start, _this.start) || other.start == _this.start)&&(identical(other.end, _this.end) || other.end == _this.end)&&(identical(other.isAllDay, _this.isAllDay) || other.isAllDay == _this.isAllDay));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,uid,title,description,start,end,isAllDay);
|
||||
int get hashCode {
|
||||
final _this = this as MarianumDate;
|
||||
return Object.hash(runtimeType,_this.uid,_this.title,_this.description,_this.start,_this.end,_this.isAllDay);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'MarianumDate(uid: $uid, title: $title, description: $description, start: $start, end: $end, isAllDay: $isAllDay)';
|
||||
final _this = this as MarianumDate;
|
||||
return 'MarianumDate(uid: ${_this.uid}, title: ${_this.title}, description: ${_this.description}, start: ${_this.start}, end: ${_this.end}, isAllDay: ${_this.isAllDay})';
|
||||
}
|
||||
|
||||
|
||||
@@ -356,7 +371,7 @@ class _$MarianumDateCopyWithImpl<$Res>
|
||||
/// Create a copy of MarianumDate
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? uid = null,Object? title = null,Object? description = freezed,Object? start = null,Object? end = null,Object? isAllDay = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(MarianumDate(
|
||||
uid: null == uid ? _self.uid : uid // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
@@ -526,23 +541,25 @@ Map<String, dynamic> toJson() {
|
||||
}
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'MarianumDate'))
|
||||
..add(DiagnosticsProperty('uid', uid))..add(DiagnosticsProperty('title', title))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('start', start))..add(DiagnosticsProperty('end', end))..add(DiagnosticsProperty('isAllDay', isAllDay));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumDate&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.start, start) || other.start == start)&&(identical(other.end, end) || other.end == end)&&(identical(other.isAllDay, isAllDay) || other.isAllDay == isAllDay));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumDate&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.start, start) || other.start == start)&&(identical(other.end, end) || other.end == end)&&(identical(other.isAllDay, isAllDay) || other.isAllDay == isAllDay));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,uid,title,description,start,end,isAllDay);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,uid,title,description,start,end,isAllDay);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'MarianumDate(uid: $uid, title: $title, description: $description, start: $start, end: $end, isAllDay: $isAllDay)';
|
||||
return 'MarianumDate(uid: $uid, title: $title, description: $description, start: $start, end: $end, isAllDay: $isAllDay)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,5 +3,3 @@ import 'marianum_message_state.dart';
|
||||
|
||||
sealed class MarianumMessageEvent
|
||||
extends LoadableHydratedBlocEvent<MarianumMessageState> {}
|
||||
|
||||
class MessageEvent extends MarianumMessageEvent {}
|
||||
@@ -37,5 +37,3 @@ abstract class MarianumMessage with _$MarianumMessage {
|
||||
factory MarianumMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$MarianumMessageFromJson(json);
|
||||
}
|
||||
|
||||
enum GradeAveragesGradingSystem { highSchool, middleSchool }
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'marianum_message_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'marianum_message_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $MarianumMessageStateCopyWith<MarianumMessageState> get copyWith => _$MarianumMe
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessageState&&(identical(other.messageList, messageList) || other.messageList == messageList));
|
||||
final _this = this as MarianumMessageState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessageState&&(identical(other.messageList, _this.messageList) || other.messageList == _this.messageList));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,messageList);
|
||||
int get hashCode {
|
||||
final _this = this as MarianumMessageState;
|
||||
return Object.hash(runtimeType,_this.messageList);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarianumMessageState(messageList: $messageList)';
|
||||
final _this = this as MarianumMessageState;
|
||||
return 'MarianumMessageState(messageList: ${_this.messageList})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$MarianumMessageStateCopyWithImpl<$Res>
|
||||
/// Create a copy of MarianumMessageState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? messageList = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(MarianumMessageState(
|
||||
messageList: null == messageList ? _self.messageList : messageList // ignore: cast_nullable_to_non_nullable
|
||||
as MarianumMessageList,
|
||||
));
|
||||
@@ -236,16 +242,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessageState&&(identical(other.messageList, messageList) || other.messageList == messageList));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessageState&&(identical(other.messageList, messageList) || other.messageList == messageList));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,messageList);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,messageList);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarianumMessageState(messageList: $messageList)';
|
||||
return 'MarianumMessageState(messageList: $messageList)';
|
||||
}
|
||||
|
||||
|
||||
@@ -309,16 +317,21 @@ $MarianumMessageListCopyWith<MarianumMessageList> get copyWith => _$MarianumMess
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessageList&&(identical(other.base, base) || other.base == base)&&const DeepCollectionEquality().equals(other.messages, messages));
|
||||
final _this = this as MarianumMessageList;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessageList&&(identical(other.base, _this.base) || other.base == _this.base)&&const DeepCollectionEquality().equals(other.messages, _this.messages));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,base,const DeepCollectionEquality().hash(messages));
|
||||
int get hashCode {
|
||||
final _this = this as MarianumMessageList;
|
||||
return Object.hash(runtimeType,_this.base,const DeepCollectionEquality().hash(_this.messages));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarianumMessageList(base: $base, messages: $messages)';
|
||||
final _this = this as MarianumMessageList;
|
||||
return 'MarianumMessageList(base: ${_this.base}, messages: ${_this.messages})';
|
||||
}
|
||||
|
||||
|
||||
@@ -347,7 +360,7 @@ class _$MarianumMessageListCopyWithImpl<$Res>
|
||||
/// Create a copy of MarianumMessageList
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? base = null,Object? messages = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(MarianumMessageList(
|
||||
base: null == base ? _self.base : base // ignore: cast_nullable_to_non_nullable
|
||||
as String,messages: null == messages ? _self.messages : messages // ignore: cast_nullable_to_non_nullable
|
||||
as List<MarianumMessage>,
|
||||
@@ -491,7 +504,7 @@ return $default(_that.base,_that.messages);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _MarianumMessageList implements MarianumMessageList {
|
||||
const _MarianumMessageList({required this.base, required final List<MarianumMessage> messages}): _messages = messages;
|
||||
const _MarianumMessageList({required this.base, required List<MarianumMessage> messages}): _messages = messages;
|
||||
factory _MarianumMessageList.fromJson(Map<String, dynamic> json) => _$MarianumMessageListFromJson(json);
|
||||
|
||||
@override final String base;
|
||||
@@ -516,16 +529,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessageList&&(identical(other.base, base) || other.base == base)&&const DeepCollectionEquality().equals(other._messages, _messages));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessageList&&(identical(other.base, base) || other.base == base)&&const DeepCollectionEquality().equals(other.messages, _messages));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,base,const DeepCollectionEquality().hash(_messages));
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,base,const DeepCollectionEquality().hash(_messages));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarianumMessageList(base: $base, messages: $messages)';
|
||||
return 'MarianumMessageList(base: $base, messages: $messages)';
|
||||
}
|
||||
|
||||
|
||||
@@ -581,16 +596,21 @@ $MarianumMessageCopyWith<MarianumMessage> get copyWith => _$MarianumMessageCopyW
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.date, date) || other.date == date)&&(identical(other.description, description) || other.description == description)&&(identical(other.url, url) || other.url == url));
|
||||
final _this = this as MarianumMessage;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MarianumMessage&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.name, _this.name) || other.name == _this.name)&&(identical(other.date, _this.date) || other.date == _this.date)&&(identical(other.description, _this.description) || other.description == _this.description)&&(identical(other.url, _this.url) || other.url == _this.url));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,date,description,url);
|
||||
int get hashCode {
|
||||
final _this = this as MarianumMessage;
|
||||
return Object.hash(runtimeType,_this.id,_this.name,_this.date,_this.description,_this.url);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarianumMessage(id: $id, name: $name, date: $date, description: $description, url: $url)';
|
||||
final _this = this as MarianumMessage;
|
||||
return 'MarianumMessage(id: ${_this.id}, name: ${_this.name}, date: ${_this.date}, description: ${_this.description}, url: ${_this.url})';
|
||||
}
|
||||
|
||||
|
||||
@@ -619,7 +639,7 @@ class _$MarianumMessageCopyWithImpl<$Res>
|
||||
/// Create a copy of MarianumMessage
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? date = null,Object? description = null,Object? url = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(MarianumMessage(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||
@@ -788,16 +808,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.date, date) || other.date == date)&&(identical(other.description, description) || other.description == description)&&(identical(other.url, url) || other.url == url));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MarianumMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.date, date) || other.date == date)&&(identical(other.description, description) || other.description == description)&&(identical(other.url, url) || other.url == url));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,date,description,url);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,id,name,date,description,url);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarianumMessage(id: $id, name: $name, date: $date, description: $description, url: $url)';
|
||||
return 'MarianumMessage(id: $id, name: $name, date: $date, description: $description, url: $url)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ import '../bloc/marianum_message_state.dart';
|
||||
class MarianumMessageGetMessages {
|
||||
final Dio _dio;
|
||||
|
||||
MarianumMessageGetMessages({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
|
||||
MarianumMessageGetMessages({Dio? dio})
|
||||
: _dio = dio ?? MarianumConnectApi.dio();
|
||||
|
||||
Future<MarianumMessageList> run() async {
|
||||
try {
|
||||
|
||||
+17
-13
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'nextcloud_capabilities_state.dart';
|
||||
@@ -9,15 +9,14 @@ part of 'nextcloud_capabilities_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$NextcloudCapabilitiesState {
|
||||
|
||||
bool get apiEnabled; bool get publicEnabled; bool get publicMultipleLinks; bool get publicUploadEnabled; bool get publicPasswordEnforced; bool get publicExpireEnabled; int? get publicExpireDays; bool get publicExpireEnforced; bool get groupEnabled; bool get resharing; int? get passwordMinLength; bool get passwordEnforceUpperLower; bool get passwordEnforceNumeric; bool get passwordEnforceSpecial;// Whether a capability response (or a definitive failure) has been observed
|
||||
// at least once this session.
|
||||
bool get loaded;
|
||||
bool get apiEnabled; bool get publicEnabled; bool get publicMultipleLinks; bool get publicUploadEnabled; bool get publicPasswordEnforced; bool get publicExpireEnabled; int? get publicExpireDays; bool get publicExpireEnforced; bool get groupEnabled; bool get resharing; int? get passwordMinLength; bool get passwordEnforceUpperLower; bool get passwordEnforceNumeric; bool get passwordEnforceSpecial; bool get loaded;
|
||||
/// Create a copy of NextcloudCapabilitiesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -30,16 +29,21 @@ $NextcloudCapabilitiesStateCopyWith<NextcloudCapabilitiesState> get copyWith =>
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is NextcloudCapabilitiesState&&(identical(other.apiEnabled, apiEnabled) || other.apiEnabled == apiEnabled)&&(identical(other.publicEnabled, publicEnabled) || other.publicEnabled == publicEnabled)&&(identical(other.publicMultipleLinks, publicMultipleLinks) || other.publicMultipleLinks == publicMultipleLinks)&&(identical(other.publicUploadEnabled, publicUploadEnabled) || other.publicUploadEnabled == publicUploadEnabled)&&(identical(other.publicPasswordEnforced, publicPasswordEnforced) || other.publicPasswordEnforced == publicPasswordEnforced)&&(identical(other.publicExpireEnabled, publicExpireEnabled) || other.publicExpireEnabled == publicExpireEnabled)&&(identical(other.publicExpireDays, publicExpireDays) || other.publicExpireDays == publicExpireDays)&&(identical(other.publicExpireEnforced, publicExpireEnforced) || other.publicExpireEnforced == publicExpireEnforced)&&(identical(other.groupEnabled, groupEnabled) || other.groupEnabled == groupEnabled)&&(identical(other.resharing, resharing) || other.resharing == resharing)&&(identical(other.passwordMinLength, passwordMinLength) || other.passwordMinLength == passwordMinLength)&&(identical(other.passwordEnforceUpperLower, passwordEnforceUpperLower) || other.passwordEnforceUpperLower == passwordEnforceUpperLower)&&(identical(other.passwordEnforceNumeric, passwordEnforceNumeric) || other.passwordEnforceNumeric == passwordEnforceNumeric)&&(identical(other.passwordEnforceSpecial, passwordEnforceSpecial) || other.passwordEnforceSpecial == passwordEnforceSpecial)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
final _this = this as NextcloudCapabilitiesState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is NextcloudCapabilitiesState&&(identical(other.apiEnabled, _this.apiEnabled) || other.apiEnabled == _this.apiEnabled)&&(identical(other.publicEnabled, _this.publicEnabled) || other.publicEnabled == _this.publicEnabled)&&(identical(other.publicMultipleLinks, _this.publicMultipleLinks) || other.publicMultipleLinks == _this.publicMultipleLinks)&&(identical(other.publicUploadEnabled, _this.publicUploadEnabled) || other.publicUploadEnabled == _this.publicUploadEnabled)&&(identical(other.publicPasswordEnforced, _this.publicPasswordEnforced) || other.publicPasswordEnforced == _this.publicPasswordEnforced)&&(identical(other.publicExpireEnabled, _this.publicExpireEnabled) || other.publicExpireEnabled == _this.publicExpireEnabled)&&(identical(other.publicExpireDays, _this.publicExpireDays) || other.publicExpireDays == _this.publicExpireDays)&&(identical(other.publicExpireEnforced, _this.publicExpireEnforced) || other.publicExpireEnforced == _this.publicExpireEnforced)&&(identical(other.groupEnabled, _this.groupEnabled) || other.groupEnabled == _this.groupEnabled)&&(identical(other.resharing, _this.resharing) || other.resharing == _this.resharing)&&(identical(other.passwordMinLength, _this.passwordMinLength) || other.passwordMinLength == _this.passwordMinLength)&&(identical(other.passwordEnforceUpperLower, _this.passwordEnforceUpperLower) || other.passwordEnforceUpperLower == _this.passwordEnforceUpperLower)&&(identical(other.passwordEnforceNumeric, _this.passwordEnforceNumeric) || other.passwordEnforceNumeric == _this.passwordEnforceNumeric)&&(identical(other.passwordEnforceSpecial, _this.passwordEnforceSpecial) || other.passwordEnforceSpecial == _this.passwordEnforceSpecial)&&(identical(other.loaded, _this.loaded) || other.loaded == _this.loaded));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,apiEnabled,publicEnabled,publicMultipleLinks,publicUploadEnabled,publicPasswordEnforced,publicExpireEnabled,publicExpireDays,publicExpireEnforced,groupEnabled,resharing,passwordMinLength,passwordEnforceUpperLower,passwordEnforceNumeric,passwordEnforceSpecial,loaded);
|
||||
int get hashCode {
|
||||
final _this = this as NextcloudCapabilitiesState;
|
||||
return Object.hash(runtimeType,_this.apiEnabled,_this.publicEnabled,_this.publicMultipleLinks,_this.publicUploadEnabled,_this.publicPasswordEnforced,_this.publicExpireEnabled,_this.publicExpireDays,_this.publicExpireEnforced,_this.groupEnabled,_this.resharing,_this.passwordMinLength,_this.passwordEnforceUpperLower,_this.passwordEnforceNumeric,_this.passwordEnforceSpecial,_this.loaded);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NextcloudCapabilitiesState(apiEnabled: $apiEnabled, publicEnabled: $publicEnabled, publicMultipleLinks: $publicMultipleLinks, publicUploadEnabled: $publicUploadEnabled, publicPasswordEnforced: $publicPasswordEnforced, publicExpireEnabled: $publicExpireEnabled, publicExpireDays: $publicExpireDays, publicExpireEnforced: $publicExpireEnforced, groupEnabled: $groupEnabled, resharing: $resharing, passwordMinLength: $passwordMinLength, passwordEnforceUpperLower: $passwordEnforceUpperLower, passwordEnforceNumeric: $passwordEnforceNumeric, passwordEnforceSpecial: $passwordEnforceSpecial, loaded: $loaded)';
|
||||
final _this = this as NextcloudCapabilitiesState;
|
||||
return 'NextcloudCapabilitiesState(apiEnabled: ${_this.apiEnabled}, publicEnabled: ${_this.publicEnabled}, publicMultipleLinks: ${_this.publicMultipleLinks}, publicUploadEnabled: ${_this.publicUploadEnabled}, publicPasswordEnforced: ${_this.publicPasswordEnforced}, publicExpireEnabled: ${_this.publicExpireEnabled}, publicExpireDays: ${_this.publicExpireDays}, publicExpireEnforced: ${_this.publicExpireEnforced}, groupEnabled: ${_this.groupEnabled}, resharing: ${_this.resharing}, passwordMinLength: ${_this.passwordMinLength}, passwordEnforceUpperLower: ${_this.passwordEnforceUpperLower}, passwordEnforceNumeric: ${_this.passwordEnforceNumeric}, passwordEnforceSpecial: ${_this.passwordEnforceSpecial}, loaded: ${_this.loaded})';
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +72,7 @@ class _$NextcloudCapabilitiesStateCopyWithImpl<$Res>
|
||||
/// Create a copy of NextcloudCapabilitiesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? apiEnabled = null,Object? publicEnabled = null,Object? publicMultipleLinks = null,Object? publicUploadEnabled = null,Object? publicPasswordEnforced = null,Object? publicExpireEnabled = null,Object? publicExpireDays = freezed,Object? publicExpireEnforced = null,Object? groupEnabled = null,Object? resharing = null,Object? passwordMinLength = freezed,Object? passwordEnforceUpperLower = null,Object? passwordEnforceNumeric = null,Object? passwordEnforceSpecial = null,Object? loaded = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(NextcloudCapabilitiesState(
|
||||
apiEnabled: null == apiEnabled ? _self.apiEnabled : apiEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,publicEnabled: null == publicEnabled ? _self.publicEnabled : publicEnabled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,publicMultipleLinks: null == publicMultipleLinks ? _self.publicMultipleLinks : publicMultipleLinks // ignore: cast_nullable_to_non_nullable
|
||||
@@ -242,8 +246,6 @@ class _NextcloudCapabilitiesState implements NextcloudCapabilitiesState {
|
||||
@override@JsonKey() final bool passwordEnforceUpperLower;
|
||||
@override@JsonKey() final bool passwordEnforceNumeric;
|
||||
@override@JsonKey() final bool passwordEnforceSpecial;
|
||||
// Whether a capability response (or a definitive failure) has been observed
|
||||
// at least once this session.
|
||||
@override@JsonKey() final bool loaded;
|
||||
|
||||
/// Create a copy of NextcloudCapabilitiesState
|
||||
@@ -259,16 +261,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NextcloudCapabilitiesState&&(identical(other.apiEnabled, apiEnabled) || other.apiEnabled == apiEnabled)&&(identical(other.publicEnabled, publicEnabled) || other.publicEnabled == publicEnabled)&&(identical(other.publicMultipleLinks, publicMultipleLinks) || other.publicMultipleLinks == publicMultipleLinks)&&(identical(other.publicUploadEnabled, publicUploadEnabled) || other.publicUploadEnabled == publicUploadEnabled)&&(identical(other.publicPasswordEnforced, publicPasswordEnforced) || other.publicPasswordEnforced == publicPasswordEnforced)&&(identical(other.publicExpireEnabled, publicExpireEnabled) || other.publicExpireEnabled == publicExpireEnabled)&&(identical(other.publicExpireDays, publicExpireDays) || other.publicExpireDays == publicExpireDays)&&(identical(other.publicExpireEnforced, publicExpireEnforced) || other.publicExpireEnforced == publicExpireEnforced)&&(identical(other.groupEnabled, groupEnabled) || other.groupEnabled == groupEnabled)&&(identical(other.resharing, resharing) || other.resharing == resharing)&&(identical(other.passwordMinLength, passwordMinLength) || other.passwordMinLength == passwordMinLength)&&(identical(other.passwordEnforceUpperLower, passwordEnforceUpperLower) || other.passwordEnforceUpperLower == passwordEnforceUpperLower)&&(identical(other.passwordEnforceNumeric, passwordEnforceNumeric) || other.passwordEnforceNumeric == passwordEnforceNumeric)&&(identical(other.passwordEnforceSpecial, passwordEnforceSpecial) || other.passwordEnforceSpecial == passwordEnforceSpecial)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NextcloudCapabilitiesState&&(identical(other.apiEnabled, apiEnabled) || other.apiEnabled == apiEnabled)&&(identical(other.publicEnabled, publicEnabled) || other.publicEnabled == publicEnabled)&&(identical(other.publicMultipleLinks, publicMultipleLinks) || other.publicMultipleLinks == publicMultipleLinks)&&(identical(other.publicUploadEnabled, publicUploadEnabled) || other.publicUploadEnabled == publicUploadEnabled)&&(identical(other.publicPasswordEnforced, publicPasswordEnforced) || other.publicPasswordEnforced == publicPasswordEnforced)&&(identical(other.publicExpireEnabled, publicExpireEnabled) || other.publicExpireEnabled == publicExpireEnabled)&&(identical(other.publicExpireDays, publicExpireDays) || other.publicExpireDays == publicExpireDays)&&(identical(other.publicExpireEnforced, publicExpireEnforced) || other.publicExpireEnforced == publicExpireEnforced)&&(identical(other.groupEnabled, groupEnabled) || other.groupEnabled == groupEnabled)&&(identical(other.resharing, resharing) || other.resharing == resharing)&&(identical(other.passwordMinLength, passwordMinLength) || other.passwordMinLength == passwordMinLength)&&(identical(other.passwordEnforceUpperLower, passwordEnforceUpperLower) || other.passwordEnforceUpperLower == passwordEnforceUpperLower)&&(identical(other.passwordEnforceNumeric, passwordEnforceNumeric) || other.passwordEnforceNumeric == passwordEnforceNumeric)&&(identical(other.passwordEnforceSpecial, passwordEnforceSpecial) || other.passwordEnforceSpecial == passwordEnforceSpecial)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,apiEnabled,publicEnabled,publicMultipleLinks,publicUploadEnabled,publicPasswordEnforced,publicExpireEnabled,publicExpireDays,publicExpireEnforced,groupEnabled,resharing,passwordMinLength,passwordEnforceUpperLower,passwordEnforceNumeric,passwordEnforceSpecial,loaded);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,apiEnabled,publicEnabled,publicMultipleLinks,publicUploadEnabled,publicPasswordEnforced,publicExpireEnabled,publicExpireDays,publicExpireEnforced,groupEnabled,resharing,passwordMinLength,passwordEnforceUpperLower,passwordEnforceNumeric,passwordEnforceSpecial,loaded);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'NextcloudCapabilitiesState(apiEnabled: $apiEnabled, publicEnabled: $publicEnabled, publicMultipleLinks: $publicMultipleLinks, publicUploadEnabled: $publicUploadEnabled, publicPasswordEnforced: $publicPasswordEnforced, publicExpireEnabled: $publicExpireEnabled, publicExpireDays: $publicExpireDays, publicExpireEnforced: $publicExpireEnforced, groupEnabled: $groupEnabled, resharing: $resharing, passwordMinLength: $passwordMinLength, passwordEnforceUpperLower: $passwordEnforceUpperLower, passwordEnforceNumeric: $passwordEnforceNumeric, passwordEnforceSpecial: $passwordEnforceSpecial, loaded: $loaded)';
|
||||
return 'NextcloudCapabilitiesState(apiEnabled: $apiEnabled, publicEnabled: $publicEnabled, publicMultipleLinks: $publicMultipleLinks, publicUploadEnabled: $publicUploadEnabled, publicPasswordEnforced: $publicPasswordEnforced, publicExpireEnabled: $publicExpireEnabled, publicExpireDays: $publicExpireDays, publicExpireEnforced: $publicExpireEnforced, groupEnabled: $groupEnabled, resharing: $resharing, passwordMinLength: $passwordMinLength, passwordEnforceUpperLower: $passwordEnforceUpperLower, passwordEnforceNumeric: $passwordEnforceNumeric, passwordEnforceSpecial: $passwordEnforceSpecial, loaded: $loaded)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,5 +2,3 @@ import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_
|
||||
import 'ticker_state.dart';
|
||||
|
||||
sealed class TickerEvent extends LoadableHydratedBlocEvent<TickerState> {}
|
||||
|
||||
class TickerLoadEvent extends TickerEvent {}
|
||||
@@ -1,6 +1,5 @@
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import 'ticker_page_state.dart';
|
||||
|
||||
sealed class TickerPageEvent extends LoadableHydratedBlocEvent<TickerPageState> {}
|
||||
|
||||
class TickerPageLoadEvent extends TickerPageEvent {}
|
||||
sealed class TickerPageEvent
|
||||
extends LoadableHydratedBlocEvent<TickerPageState> {}
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'ticker_page_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'ticker_page_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $TickerPageStateCopyWith<TickerPageState> get copyWith => _$TickerPageStateCopyW
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TickerPageState&&(identical(other.page, page) || other.page == page));
|
||||
final _this = this as TickerPageState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TickerPageState&&(identical(other.page, _this.page) || other.page == _this.page));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,page);
|
||||
int get hashCode {
|
||||
final _this = this as TickerPageState;
|
||||
return Object.hash(runtimeType,_this.page);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TickerPageState(page: $page)';
|
||||
final _this = this as TickerPageState;
|
||||
return 'TickerPageState(page: ${_this.page})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$TickerPageStateCopyWithImpl<$Res>
|
||||
/// Create a copy of TickerPageState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? page = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(TickerPageState(
|
||||
page: freezed == page ? _self.page : page // ignore: cast_nullable_to_non_nullable
|
||||
as TickerPageResponse?,
|
||||
));
|
||||
@@ -227,16 +233,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TickerPageState&&(identical(other.page, page) || other.page == page));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TickerPageState&&(identical(other.page, page) || other.page == page));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,page);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,page);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TickerPageState(page: $page)';
|
||||
return 'TickerPageState(page: $page)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'ticker_state.dart';
|
||||
@@ -9,6 +9,7 @@ part of 'ticker_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
@@ -28,16 +29,21 @@ $TickerStateCopyWith<TickerState> get copyWith => _$TickerStateCopyWithImpl<Tick
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TickerState&&(identical(other.ticker, ticker) || other.ticker == ticker)&&(identical(other.nav, nav) || other.nav == nav)&&(identical(other.selectedSlug, selectedSlug) || other.selectedSlug == selectedSlug));
|
||||
final _this = this as TickerState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TickerState&&(identical(other.ticker, _this.ticker) || other.ticker == _this.ticker)&&(identical(other.nav, _this.nav) || other.nav == _this.nav)&&(identical(other.selectedSlug, _this.selectedSlug) || other.selectedSlug == _this.selectedSlug));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,ticker,nav,selectedSlug);
|
||||
int get hashCode {
|
||||
final _this = this as TickerState;
|
||||
return Object.hash(runtimeType,_this.ticker,_this.nav,_this.selectedSlug);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TickerState(ticker: $ticker, nav: $nav, selectedSlug: $selectedSlug)';
|
||||
final _this = this as TickerState;
|
||||
return 'TickerState(ticker: ${_this.ticker}, nav: ${_this.nav}, selectedSlug: ${_this.selectedSlug})';
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +72,7 @@ class _$TickerStateCopyWithImpl<$Res>
|
||||
/// Create a copy of TickerState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? ticker = freezed,Object? nav = freezed,Object? selectedSlug = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(TickerState(
|
||||
ticker: freezed == ticker ? _self.ticker : ticker // ignore: cast_nullable_to_non_nullable
|
||||
as TickerResponse?,nav: freezed == nav ? _self.nav : nav // ignore: cast_nullable_to_non_nullable
|
||||
as TickerNavResponse?,selectedSlug: freezed == selectedSlug ? _self.selectedSlug : selectedSlug // ignore: cast_nullable_to_non_nullable
|
||||
@@ -231,16 +237,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TickerState&&(identical(other.ticker, ticker) || other.ticker == ticker)&&(identical(other.nav, nav) || other.nav == nav)&&(identical(other.selectedSlug, selectedSlug) || other.selectedSlug == selectedSlug));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TickerState&&(identical(other.ticker, ticker) || other.ticker == ticker)&&(identical(other.nav, nav) || other.nav == nav)&&(identical(other.selectedSlug, selectedSlug) || other.selectedSlug == selectedSlug));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,ticker,nav,selectedSlug);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,ticker,nav,selectedSlug);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TickerState(ticker: $ticker, nav: $nav, selectedSlug: $selectedSlug)';
|
||||
return 'TickerState(ticker: $ticker, nav: $nav, selectedSlug: $selectedSlug)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import '../../../../../api/marianumconnect/queries/timetable_custom_events/custom_events_migration.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../../extensions/date_time.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../data_provider/timetable_data_provider.dart';
|
||||
import '../repository/timetable_repository.dart';
|
||||
import 'timetable_event.dart';
|
||||
import 'timetable_state.dart';
|
||||
import 'timetable_week_loader.dart';
|
||||
|
||||
class TimetableBloc
|
||||
extends
|
||||
@@ -16,11 +15,10 @@ class TimetableBloc
|
||||
TimetableEvent,
|
||||
TimetableState,
|
||||
TimetableRepository
|
||||
> {
|
||||
DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
/// Set by [retry] to force the next [gatherData] to bypass cache freshness
|
||||
/// checks and actually hit the network. Cleared at the top of [gatherData].
|
||||
>
|
||||
with TimetableWeekLoader<TimetableRepository> {
|
||||
/// Set by [retry] to force the next [gatherData] to bypass the custom-event
|
||||
/// cache and actually hit the network. Cleared at the top of [gatherData].
|
||||
bool _forceRenew = false;
|
||||
|
||||
@override
|
||||
@@ -33,24 +31,21 @@ class TimetableBloc
|
||||
TimetableRepository repository() => TimetableRepository();
|
||||
|
||||
@override
|
||||
TimetableState fromNothing() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
return TimetableState(
|
||||
startDate: reference.mondayOfWeek,
|
||||
endDate: _endOfWeek(reference),
|
||||
);
|
||||
}
|
||||
TimetableDataProvider get referenceData => repo.data;
|
||||
|
||||
@override
|
||||
Future<TimetableGetWeekResponse> fetchWeek(DateTime start, DateTime end) =>
|
||||
repo.data.getWeek(start, end);
|
||||
|
||||
/// Persisted state may carry a stale `startDate`/`endDate` from the user's
|
||||
/// last view. Reset on every cold start so the calendar always mounts on
|
||||
/// the current week, not on whatever week the user closed the app on.
|
||||
@override
|
||||
TimetableState fromStorage(Map<String, dynamic> json) {
|
||||
final stored = TimetableState.fromJson(json);
|
||||
final reference = DateTime.now().addDays(2);
|
||||
return stored.copyWith(
|
||||
startDate: reference.mondayOfWeek,
|
||||
endDate: _endOfWeek(reference),
|
||||
final (start, end) = currentTimetableWeek();
|
||||
return TimetableState.fromJson(json).copyWith(
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
accessibleStartDate: null,
|
||||
accessibleEndDate: null,
|
||||
);
|
||||
@@ -60,50 +55,14 @@ class TimetableBloc
|
||||
Map<String, dynamic>? toStorage(TimetableState state) => state.toJson();
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final initial = innerState ?? fromNothing();
|
||||
Future<void> gatherData() {
|
||||
final renew = _forceRenew;
|
||||
_forceRenew = false;
|
||||
|
||||
Object? firstError;
|
||||
void recordError(Object e) {
|
||||
firstError ??= e;
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_loadCurrentWeek(
|
||||
initial.startDate,
|
||||
initial.endDate,
|
||||
onError: recordError,
|
||||
renew: renew,
|
||||
),
|
||||
_loadStaticReferenceData(onError: recordError, renew: renew),
|
||||
_loadCustomEvents(onError: recordError, renew: renew),
|
||||
]);
|
||||
|
||||
if (firstError != null) throw firstError!;
|
||||
|
||||
add(DataGathered((s) => s));
|
||||
_prefetchAdjacentWeeks(initial.startDate, initial.endDate);
|
||||
return gatherWeek(
|
||||
also: (onError) => _loadCustomEvents(onError: onError, renew: renew),
|
||||
);
|
||||
}
|
||||
|
||||
void changeWeek(DateTime startDate, DateTime endDate) {
|
||||
final current = innerState ?? fromNothing();
|
||||
if (current.startDate == startDate && current.endDate == endDate) return;
|
||||
add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate)));
|
||||
runInSession(() {
|
||||
_loadCurrentWeek(startDate, endDate);
|
||||
_prefetchAdjacentWeeks(startDate, endDate);
|
||||
});
|
||||
}
|
||||
|
||||
void resetWeek() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
changeWeek(reference.mondayOfWeek, _endOfWeek(reference));
|
||||
}
|
||||
|
||||
void refresh() => fetch();
|
||||
|
||||
Future<void> addCustomEvent(CustomTimetableEvent event) async {
|
||||
await repo.data.addCustomEvent(event);
|
||||
await _refreshCustomEvents();
|
||||
@@ -132,67 +91,10 @@ class TimetableBloc
|
||||
}
|
||||
|
||||
Future<void> _refreshSubjects() async {
|
||||
final subjects = await repo.data.getSubjects(renew: true);
|
||||
final subjects = await repo.data.getSubjects();
|
||||
add(DataGathered((s) => s.withReferenceData(subjects: subjects)));
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentWeek(
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
final requestStart = DateTime.now();
|
||||
_lastWeekRequestStart = requestStart;
|
||||
try {
|
||||
final week = await repo.data.getWeek(
|
||||
startDate,
|
||||
endDate,
|
||||
onError: onError,
|
||||
renew: renew,
|
||||
);
|
||||
if (_lastWeekRequestStart.isAfter(requestStart)) return;
|
||||
_writeWeekToCache(startDate, week);
|
||||
} catch (e) {
|
||||
log('getWeek error for $startDate–$endDate: $e');
|
||||
onError?.call(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadStaticReferenceData({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
final (rooms, subjects, schoolHolidays, schoolyear) = await (
|
||||
repo.data.getRooms(onError: onError, renew: renew),
|
||||
repo.data.getSubjects(onError: onError, renew: renew),
|
||||
repo.data.getSchoolHolidays(onError: onError, renew: renew),
|
||||
repo.data.getCurrentSchoolyear(onError: onError, renew: renew),
|
||||
).wait;
|
||||
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.withReferenceData(
|
||||
rooms: rooms,
|
||||
subjects: subjects,
|
||||
schoolHolidays: schoolHolidays,
|
||||
schoolyear: schoolyear,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
}
|
||||
|
||||
try {
|
||||
final timegrid = await repo.data.getTimegrid(renew: renew);
|
||||
add(Emit((s) => s.withReferenceData(timegrid: timegrid)));
|
||||
} catch (_) {
|
||||
// Timegrid load failure falls back to a hardcoded schedule in the UI layer.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCustomEvents({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
@@ -213,27 +115,4 @@ class TimetableBloc
|
||||
final events = await repo.data.getCustomEvents(renew: true);
|
||||
add(DataGathered((s) => s.withReferenceData(customEvents: events)));
|
||||
}
|
||||
|
||||
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
|
||||
_prefetchWeek(start.subtractDays(7), end.subtractDays(7));
|
||||
_prefetchWeek(start.addDays(7), end.addDays(7));
|
||||
}
|
||||
|
||||
void _prefetchWeek(DateTime start, DateTime end) {
|
||||
repo.data
|
||||
.getWeek(start, end)
|
||||
.then((week) => _writeWeekToCache(start, week))
|
||||
.catchError((_) {});
|
||||
}
|
||||
|
||||
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
|
||||
add(Emit((s) => s.withFetchedWeek(weekStart, week)));
|
||||
}
|
||||
|
||||
static DateTime _endOfWeek(DateTime reference) {
|
||||
final friday = reference.addDays(
|
||||
DateTime.daysPerWeek - reference.weekday - 2,
|
||||
);
|
||||
return DateTime(friday.year, friday.month, friday.day);
|
||||
}
|
||||
}
|
||||
@@ -98,9 +98,9 @@ abstract class TimetableState with _$TimetableState {
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasReferenceData =>
|
||||
rooms != null &&
|
||||
subjects != null &&
|
||||
schoolHolidays != null &&
|
||||
customEvents != null;
|
||||
/// Everything a foreign plan needs to render; it never has custom events.
|
||||
bool get hasSchoolReferenceData =>
|
||||
rooms != null && subjects != null && schoolHolidays != null;
|
||||
|
||||
bool get hasReferenceData => hasSchoolReferenceData && customEvents != null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'timetable_state.dart';
|
||||
@@ -9,16 +9,14 @@ part of 'timetable_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TimetableState {
|
||||
|
||||
Map<String, TimetableGetWeekResponse> get weekCache; TimetableGetRoomsResponse? get rooms; TimetableGetSubjectsResponse? get subjects; TimetableGetHolidaysResponse? get schoolHolidays; TimetableGetSchoolyearResponse? get schoolyear; TimetableGetTimegridResponse? get timegrid; GetCustomTimetableEventResponse? get customEvents; DateTime get startDate; DateTime get endDate; int get dataVersion;// Boundaries learned from past server denials. A week is permitted when its
|
||||
// start is on/before `accessibleEndDate` and its end on/after
|
||||
// `accessibleStartDate`. Null = that bound not discovered yet.
|
||||
DateTime? get accessibleStartDate; DateTime? get accessibleEndDate;
|
||||
Map<String, TimetableGetWeekResponse> get weekCache; TimetableGetRoomsResponse? get rooms; TimetableGetSubjectsResponse? get subjects; TimetableGetHolidaysResponse? get schoolHolidays; TimetableGetSchoolyearResponse? get schoolyear; TimetableGetTimegridResponse? get timegrid; GetCustomTimetableEventResponse? get customEvents; DateTime get startDate; DateTime get endDate; int get dataVersion; DateTime? get accessibleStartDate; DateTime? get accessibleEndDate;
|
||||
/// Create a copy of TimetableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -31,16 +29,21 @@ $TimetableStateCopyWith<TimetableState> get copyWith => _$TimetableStateCopyWith
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TimetableState&&const DeepCollectionEquality().equals(other.weekCache, weekCache)&&(identical(other.rooms, rooms) || other.rooms == rooms)&&(identical(other.subjects, subjects) || other.subjects == subjects)&&(identical(other.schoolHolidays, schoolHolidays) || other.schoolHolidays == schoolHolidays)&&(identical(other.schoolyear, schoolyear) || other.schoolyear == schoolyear)&&(identical(other.timegrid, timegrid) || other.timegrid == timegrid)&&(identical(other.customEvents, customEvents) || other.customEvents == customEvents)&&(identical(other.startDate, startDate) || other.startDate == startDate)&&(identical(other.endDate, endDate) || other.endDate == endDate)&&(identical(other.dataVersion, dataVersion) || other.dataVersion == dataVersion)&&(identical(other.accessibleStartDate, accessibleStartDate) || other.accessibleStartDate == accessibleStartDate)&&(identical(other.accessibleEndDate, accessibleEndDate) || other.accessibleEndDate == accessibleEndDate));
|
||||
final _this = this as TimetableState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TimetableState&&const DeepCollectionEquality().equals(other.weekCache, _this.weekCache)&&(identical(other.rooms, _this.rooms) || other.rooms == _this.rooms)&&(identical(other.subjects, _this.subjects) || other.subjects == _this.subjects)&&(identical(other.schoolHolidays, _this.schoolHolidays) || other.schoolHolidays == _this.schoolHolidays)&&(identical(other.schoolyear, _this.schoolyear) || other.schoolyear == _this.schoolyear)&&(identical(other.timegrid, _this.timegrid) || other.timegrid == _this.timegrid)&&(identical(other.customEvents, _this.customEvents) || other.customEvents == _this.customEvents)&&(identical(other.startDate, _this.startDate) || other.startDate == _this.startDate)&&(identical(other.endDate, _this.endDate) || other.endDate == _this.endDate)&&(identical(other.dataVersion, _this.dataVersion) || other.dataVersion == _this.dataVersion)&&(identical(other.accessibleStartDate, _this.accessibleStartDate) || other.accessibleStartDate == _this.accessibleStartDate)&&(identical(other.accessibleEndDate, _this.accessibleEndDate) || other.accessibleEndDate == _this.accessibleEndDate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(weekCache),rooms,subjects,schoolHolidays,schoolyear,timegrid,customEvents,startDate,endDate,dataVersion,accessibleStartDate,accessibleEndDate);
|
||||
int get hashCode {
|
||||
final _this = this as TimetableState;
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_this.weekCache),_this.rooms,_this.subjects,_this.schoolHolidays,_this.schoolyear,_this.timegrid,_this.customEvents,_this.startDate,_this.endDate,_this.dataVersion,_this.accessibleStartDate,_this.accessibleEndDate);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TimetableState(weekCache: $weekCache, rooms: $rooms, subjects: $subjects, schoolHolidays: $schoolHolidays, schoolyear: $schoolyear, timegrid: $timegrid, customEvents: $customEvents, startDate: $startDate, endDate: $endDate, dataVersion: $dataVersion, accessibleStartDate: $accessibleStartDate, accessibleEndDate: $accessibleEndDate)';
|
||||
final _this = this as TimetableState;
|
||||
return 'TimetableState(weekCache: ${_this.weekCache}, rooms: ${_this.rooms}, subjects: ${_this.subjects}, schoolHolidays: ${_this.schoolHolidays}, schoolyear: ${_this.schoolyear}, timegrid: ${_this.timegrid}, customEvents: ${_this.customEvents}, startDate: ${_this.startDate}, endDate: ${_this.endDate}, dataVersion: ${_this.dataVersion}, accessibleStartDate: ${_this.accessibleStartDate}, accessibleEndDate: ${_this.accessibleEndDate})';
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +72,7 @@ class _$TimetableStateCopyWithImpl<$Res>
|
||||
/// Create a copy of TimetableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? weekCache = null,Object? rooms = freezed,Object? subjects = freezed,Object? schoolHolidays = freezed,Object? schoolyear = freezed,Object? timegrid = freezed,Object? customEvents = freezed,Object? startDate = null,Object? endDate = null,Object? dataVersion = null,Object? accessibleStartDate = freezed,Object? accessibleEndDate = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
return _then(TimetableState(
|
||||
weekCache: null == weekCache ? _self.weekCache : weekCache // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, TimetableGetWeekResponse>,rooms: freezed == rooms ? _self.rooms : rooms // ignore: cast_nullable_to_non_nullable
|
||||
as TimetableGetRoomsResponse?,subjects: freezed == subjects ? _self.subjects : subjects // ignore: cast_nullable_to_non_nullable
|
||||
@@ -223,7 +226,7 @@ return $default(_that.weekCache,_that.rooms,_that.subjects,_that.schoolHolidays,
|
||||
@JsonSerializable()
|
||||
|
||||
class _TimetableState extends TimetableState {
|
||||
const _TimetableState({final Map<String, TimetableGetWeekResponse> weekCache = const <String, TimetableGetWeekResponse>{}, this.rooms, this.subjects, this.schoolHolidays, this.schoolyear, this.timegrid, this.customEvents, required this.startDate, required this.endDate, this.dataVersion = 0, this.accessibleStartDate, this.accessibleEndDate}): _weekCache = weekCache,super._();
|
||||
const _TimetableState({ Map<String, TimetableGetWeekResponse> weekCache = const <String, TimetableGetWeekResponse>{}, this.rooms, this.subjects, this.schoolHolidays, this.schoolyear, this.timegrid, this.customEvents, required this.startDate, required this.endDate, this.dataVersion = 0, this.accessibleStartDate, this.accessibleEndDate}): _weekCache = weekCache,super._();
|
||||
factory _TimetableState.fromJson(Map<String, dynamic> json) => _$TimetableStateFromJson(json);
|
||||
|
||||
final Map<String, TimetableGetWeekResponse> _weekCache;
|
||||
@@ -242,9 +245,6 @@ class _TimetableState extends TimetableState {
|
||||
@override final DateTime startDate;
|
||||
@override final DateTime endDate;
|
||||
@override@JsonKey() final int dataVersion;
|
||||
// Boundaries learned from past server denials. A week is permitted when its
|
||||
// start is on/before `accessibleEndDate` and its end on/after
|
||||
// `accessibleStartDate`. Null = that bound not discovered yet.
|
||||
@override final DateTime? accessibleStartDate;
|
||||
@override final DateTime? accessibleEndDate;
|
||||
|
||||
@@ -261,16 +261,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TimetableState&&const DeepCollectionEquality().equals(other._weekCache, _weekCache)&&(identical(other.rooms, rooms) || other.rooms == rooms)&&(identical(other.subjects, subjects) || other.subjects == subjects)&&(identical(other.schoolHolidays, schoolHolidays) || other.schoolHolidays == schoolHolidays)&&(identical(other.schoolyear, schoolyear) || other.schoolyear == schoolyear)&&(identical(other.timegrid, timegrid) || other.timegrid == timegrid)&&(identical(other.customEvents, customEvents) || other.customEvents == customEvents)&&(identical(other.startDate, startDate) || other.startDate == startDate)&&(identical(other.endDate, endDate) || other.endDate == endDate)&&(identical(other.dataVersion, dataVersion) || other.dataVersion == dataVersion)&&(identical(other.accessibleStartDate, accessibleStartDate) || other.accessibleStartDate == accessibleStartDate)&&(identical(other.accessibleEndDate, accessibleEndDate) || other.accessibleEndDate == accessibleEndDate));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TimetableState&&const DeepCollectionEquality().equals(other.weekCache, _weekCache)&&(identical(other.rooms, rooms) || other.rooms == rooms)&&(identical(other.subjects, subjects) || other.subjects == subjects)&&(identical(other.schoolHolidays, schoolHolidays) || other.schoolHolidays == schoolHolidays)&&(identical(other.schoolyear, schoolyear) || other.schoolyear == schoolyear)&&(identical(other.timegrid, timegrid) || other.timegrid == timegrid)&&(identical(other.customEvents, customEvents) || other.customEvents == customEvents)&&(identical(other.startDate, startDate) || other.startDate == startDate)&&(identical(other.endDate, endDate) || other.endDate == endDate)&&(identical(other.dataVersion, dataVersion) || other.dataVersion == dataVersion)&&(identical(other.accessibleStartDate, accessibleStartDate) || other.accessibleStartDate == accessibleStartDate)&&(identical(other.accessibleEndDate, accessibleEndDate) || other.accessibleEndDate == accessibleEndDate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_weekCache),rooms,subjects,schoolHolidays,schoolyear,timegrid,customEvents,startDate,endDate,dataVersion,accessibleStartDate,accessibleEndDate);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,const DeepCollectionEquality().hash(_weekCache),rooms,subjects,schoolHolidays,schoolyear,timegrid,customEvents,startDate,endDate,dataVersion,accessibleStartDate,accessibleEndDate);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TimetableState(weekCache: $weekCache, rooms: $rooms, subjects: $subjects, schoolHolidays: $schoolHolidays, schoolyear: $schoolyear, timegrid: $timegrid, customEvents: $customEvents, startDate: $startDate, endDate: $endDate, dataVersion: $dataVersion, accessibleStartDate: $accessibleStartDate, accessibleEndDate: $accessibleEndDate)';
|
||||
return 'TimetableState(weekCache: $weekCache, rooms: $rooms, subjects: $subjects, schoolHolidays: $schoolHolidays, schoolyear: $schoolyear, timegrid: $timegrid, customEvents: $customEvents, startDate: $startDate, endDate: $endDate, dataVersion: $dataVersion, accessibleStartDate: $accessibleStartDate, accessibleEndDate: $accessibleEndDate)';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../../extensions/date_time.dart';
|
||||
import '../../../infrastructure/repository/repository.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../data_provider/timetable_data_provider.dart';
|
||||
import 'timetable_event.dart';
|
||||
import 'timetable_state.dart';
|
||||
|
||||
/// The date whose week the timetable opens on — two days ahead, so Saturday
|
||||
/// and Sunday already show the next week.
|
||||
DateTime timetableAnchorDate() => DateTime.now().addDays(2);
|
||||
|
||||
/// Monday 00:00 to Friday 00:00 of the week the timetable opens on.
|
||||
(DateTime, DateTime) currentTimetableWeek() {
|
||||
final reference = timetableAnchorDate();
|
||||
final friday = reference.addDays(
|
||||
DateTime.daysPerWeek - reference.weekday - 2,
|
||||
);
|
||||
return (reference.mondayOfWeek, friday.dateOnly);
|
||||
}
|
||||
|
||||
/// Week navigation, week loading/prefetching and the school-wide reference
|
||||
/// data, shared by the own and the foreign timetable.
|
||||
mixin TimetableWeekLoader<R extends Repository<TimetableState>>
|
||||
on LoadableHydratedBloc<TimetableEvent, TimetableState, R> {
|
||||
DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
Future<TimetableGetWeekResponse> fetchWeek(DateTime start, DateTime end);
|
||||
|
||||
TimetableDataProvider get referenceData;
|
||||
|
||||
@override
|
||||
TimetableState fromNothing() {
|
||||
final (start, end) = currentTimetableWeek();
|
||||
return TimetableState(startDate: start, endDate: end);
|
||||
}
|
||||
|
||||
/// Loads the viewed week and the reference data (plus [also]), fails with
|
||||
/// the first error of any of them and prefetches the neighbouring weeks.
|
||||
Future<void> gatherWeek({
|
||||
Future<void> Function(void Function(Object) onError)? also,
|
||||
}) async {
|
||||
final initial = innerState ?? fromNothing();
|
||||
|
||||
Object? firstError;
|
||||
void recordError(Object e) => firstError ??= e;
|
||||
|
||||
await Future.wait([
|
||||
_loadWeek(initial.startDate, initial.endDate, onError: recordError),
|
||||
_loadReferenceData(onError: recordError),
|
||||
if (also != null) also(recordError),
|
||||
]);
|
||||
|
||||
if (firstError != null) throw firstError!;
|
||||
|
||||
add(DataGathered((s) => s));
|
||||
_prefetchAdjacentWeeks(initial.startDate, initial.endDate);
|
||||
}
|
||||
|
||||
void changeWeek(DateTime startDate, DateTime endDate) {
|
||||
final current = innerState ?? fromNothing();
|
||||
if (current.startDate == startDate && current.endDate == endDate) return;
|
||||
add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate)));
|
||||
runInSession(() {
|
||||
_loadWeek(startDate, endDate);
|
||||
_prefetchAdjacentWeeks(startDate, endDate);
|
||||
});
|
||||
}
|
||||
|
||||
void refresh() => fetch();
|
||||
|
||||
Future<void> _loadWeek(
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
final requestStart = DateTime.now();
|
||||
_lastWeekRequestStart = requestStart;
|
||||
try {
|
||||
final week = await fetchWeek(startDate, endDate);
|
||||
if (_lastWeekRequestStart.isAfter(requestStart)) return;
|
||||
_writeWeekToCache(startDate, week);
|
||||
} catch (e) {
|
||||
log('Week load error for $startDate–$endDate: $e');
|
||||
onError?.call(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadReferenceData({void Function(Object)? onError}) async {
|
||||
try {
|
||||
final (rooms, subjects, schoolHolidays, schoolyear) = await (
|
||||
referenceData.getRooms(onError: onError),
|
||||
referenceData.getSubjects(onError: onError),
|
||||
referenceData.getSchoolHolidays(onError: onError),
|
||||
referenceData.getCurrentSchoolyear(onError: onError),
|
||||
).wait;
|
||||
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.withReferenceData(
|
||||
rooms: rooms,
|
||||
subjects: subjects,
|
||||
schoolHolidays: schoolHolidays,
|
||||
schoolyear: schoolyear,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
}
|
||||
|
||||
try {
|
||||
final timegrid = await referenceData.getTimegrid();
|
||||
add(Emit((s) => s.withReferenceData(timegrid: timegrid)));
|
||||
} catch (_) {
|
||||
// Timegrid load failure falls back to a hardcoded schedule in the UI.
|
||||
}
|
||||
}
|
||||
|
||||
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
|
||||
_prefetchWeek(start.subtractDays(7), end.subtractDays(7));
|
||||
_prefetchWeek(start.addDays(7), end.addDays(7));
|
||||
}
|
||||
|
||||
void _prefetchWeek(DateTime start, DateTime end) {
|
||||
fetchWeek(
|
||||
start,
|
||||
end,
|
||||
).then((week) => _writeWeekToCache(start, week)).catchError((_) {});
|
||||
}
|
||||
|
||||
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
|
||||
add(Emit((s) => s.withFetchedWeek(weekStart, week)));
|
||||
}
|
||||
}
|
||||
@@ -23,72 +23,31 @@ import '../../../../../api/mhsl/custom_timetable_event/get/get_custom_timetable_
|
||||
import '../../../../../api/request_cache.dart';
|
||||
|
||||
/// Pulls the timetable from the Marianum-Connect mobile API. Each endpoint is
|
||||
/// its own HTTP call; this provider exposes the lazy futures so the bloc can
|
||||
/// chain them without seeing the dio layer.
|
||||
/// its own HTTP call; this provider exposes the futures so the bloc can chain
|
||||
/// them without seeing the dio layer.
|
||||
class TimetableDataProvider {
|
||||
Future<TimetableGetWeekResponse> getWeek(
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetWeek().run(from: startDate, until: endDate);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
DateTime endDate,
|
||||
) async => TimetableGetWeek().run(from: startDate, until: endDate);
|
||||
|
||||
Future<TimetableGetRoomsResponse> getRooms({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetRooms().run();
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}) => _reportingErrors(TimetableGetRooms().run, onError);
|
||||
|
||||
Future<TimetableGetSubjectsResponse> getSubjects({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetSubjects().run();
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}) => _reportingErrors(TimetableGetSubjects().run, onError);
|
||||
|
||||
Future<TimetableGetHolidaysResponse> getSchoolHolidays({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetHolidays().run();
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}) => _reportingErrors(TimetableGetHolidays().run, onError);
|
||||
|
||||
Future<TimetableGetSchoolyearResponse> getCurrentSchoolyear({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetSchoolyear().run();
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}) => _reportingErrors(TimetableGetSchoolyear().run, onError);
|
||||
|
||||
Future<TimetableGetTimegridResponse> getTimegrid({bool renew = false}) =>
|
||||
Future<TimetableGetTimegridResponse> getTimegrid() =>
|
||||
TimetableGetTimegrid().run();
|
||||
|
||||
Future<GetCustomTimetableEventResponse> getCustomEvents({
|
||||
@@ -131,4 +90,18 @@ class TimetableDataProvider {
|
||||
if (DemoMode.active) return Future.value();
|
||||
return TimetableSubjectColorRemove().run(subjectShort);
|
||||
}
|
||||
|
||||
// Reports the original error before rethrowing: callers await several of
|
||||
// these together, and a parallel wait only surfaces a combined error.
|
||||
static Future<T> _reportingErrors<T>(
|
||||
Future<T> Function() request,
|
||||
void Function(Object)? onError,
|
||||
) async {
|
||||
try {
|
||||
return await request();
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,14 +88,10 @@ class DevToolsSettings {
|
||||
if (trimmed.isEmpty) return null;
|
||||
final uri = Uri.tryParse(trimmed);
|
||||
if (uri == null || !uri.hasScheme) return null;
|
||||
if (uri.scheme == 'https') {
|
||||
// always fine
|
||||
} else if (uri.scheme == 'http' && allowsHttpCustomEndpoint) {
|
||||
// dev only
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (uri.host.isEmpty) return null;
|
||||
final schemeAllowed =
|
||||
uri.scheme == 'https' ||
|
||||
(uri.scheme == 'http' && allowsHttpCustomEndpoint);
|
||||
if (!schemeAllowed || uri.host.isEmpty) return null;
|
||||
return trimmed.endsWith('/')
|
||||
? trimmed.substring(0, trimmed.length - 1)
|
||||
: trimmed;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../state/app/modules/app_modules.dart';
|
||||
import '../state/app/modules/app_modules.dart';
|
||||
|
||||
part 'modules_settings.g.dart';
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ class TimetableFavoritesSettings {
|
||||
/// Adds or removes the element in place. Returns the new favorite state
|
||||
/// (`true` = now starred). Callers persist via `SettingsCubit.val(write: true)`.
|
||||
bool toggle(TimetableElementType type, int id, String label) {
|
||||
final existing = favorites.where((f) => f.type == type && f.id == id);
|
||||
if (existing.isNotEmpty) {
|
||||
if (isFavorite(type, id)) {
|
||||
favorites.removeWhere((f) => f.type == type && f.id == id);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../view/pages/timetable/data/timetable_name_mode.dart';
|
||||
import '../view/pages/timetable/data/timetable_name_mode.dart';
|
||||
|
||||
part 'timetable_settings.g.dart';
|
||||
|
||||
|
||||
+14
-24
@@ -10,31 +10,21 @@ class AppSpacing {
|
||||
static const double xl = 40;
|
||||
}
|
||||
|
||||
TextStyle inputErrorStyle(BuildContext context) =>
|
||||
TextStyle(color: Theme.of(context).colorScheme.error);
|
||||
|
||||
class AppTheme {
|
||||
static DropdownDisplay getDisplayOptions(ThemeMode theme) {
|
||||
switch (theme) {
|
||||
case ThemeMode.system:
|
||||
return DropdownDisplay(
|
||||
icon: Icons.auto_fix_high_outlined,
|
||||
displayName: 'Systemvorgabe',
|
||||
);
|
||||
|
||||
case ThemeMode.light:
|
||||
return DropdownDisplay(
|
||||
icon: Icons.wb_sunny_outlined,
|
||||
displayName: 'Hell',
|
||||
);
|
||||
|
||||
case ThemeMode.dark:
|
||||
return DropdownDisplay(
|
||||
icon: Icons.dark_mode_outlined,
|
||||
displayName: 'Dunkel',
|
||||
);
|
||||
}
|
||||
}
|
||||
static DropdownDisplay getDisplayOptions(ThemeMode theme) => switch (theme) {
|
||||
ThemeMode.system => DropdownDisplay(
|
||||
icon: Icons.auto_fix_high_outlined,
|
||||
displayName: 'Systemvorgabe',
|
||||
),
|
||||
ThemeMode.light => DropdownDisplay(
|
||||
icon: Icons.wb_sunny_outlined,
|
||||
displayName: 'Hell',
|
||||
),
|
||||
ThemeMode.dark => DropdownDisplay(
|
||||
icon: Icons.dark_mode_outlined,
|
||||
displayName: 'Dunkel',
|
||||
),
|
||||
};
|
||||
|
||||
static bool isDarkMode(BuildContext context) =>
|
||||
Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
@@ -35,11 +35,7 @@ class DownloadFailed extends DownloadStatus {
|
||||
/// the same job. Kept engine-agnostic on purpose — the native
|
||||
/// `background_downloader` task lives entirely inside [DownloadManager].
|
||||
class DownloadJob {
|
||||
DownloadJob({
|
||||
required this.remotePath,
|
||||
required this.name,
|
||||
this.remoteFile,
|
||||
});
|
||||
DownloadJob({required this.remotePath, required this.name, this.remoteFile});
|
||||
|
||||
final String remotePath;
|
||||
final String name;
|
||||
@@ -59,10 +55,7 @@ class DownloadJob {
|
||||
const DownloadInProgress(0),
|
||||
);
|
||||
|
||||
bool get isFinished =>
|
||||
status.value is DownloadDone ||
|
||||
status.value is DownloadFailed ||
|
||||
status.value is DownloadCancelled;
|
||||
bool get isFinished => status.value is! DownloadInProgress;
|
||||
|
||||
bool get isDone => status.value is DownloadDone;
|
||||
bool get isFailed => status.value is DownloadFailed;
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart' as bd;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../notification/notification_service.dart';
|
||||
@@ -34,6 +35,10 @@ class DownloadManager {
|
||||
/// "Teilen"/"Speichern" actions.
|
||||
static const _directory = 'downloads';
|
||||
|
||||
/// Where finished downloads land (`BaseDirectory.temporary` + [_directory]).
|
||||
static Future<Directory> downloadsDirectory() async =>
|
||||
Directory('${(await getTemporaryDirectory()).path}/$_directory');
|
||||
|
||||
final Map<String, DownloadJob> _jobs = {}; // keyed by remotePath
|
||||
final Map<String, DownloadJob> _byTaskId = {};
|
||||
|
||||
@@ -237,9 +242,7 @@ class DownloadManager {
|
||||
// instead of freezing at the last percent.
|
||||
job.status.value = const DownloadCancelled();
|
||||
}
|
||||
_jobs.remove(job.remotePath);
|
||||
if (taskId != null) _byTaskId.remove(taskId);
|
||||
scheduleMicrotask(job.dispose);
|
||||
_forget(job);
|
||||
}
|
||||
_refreshVisible();
|
||||
}
|
||||
@@ -330,10 +333,14 @@ class DownloadManager {
|
||||
// --- bookkeeping -----------------------------------------------------------
|
||||
|
||||
void _remove(DownloadJob job) {
|
||||
_forget(job);
|
||||
_refreshVisible();
|
||||
}
|
||||
|
||||
void _forget(DownloadJob job) {
|
||||
_jobs.remove(job.remotePath);
|
||||
final taskId = job.taskId;
|
||||
if (taskId != null) _byTaskId.remove(taskId);
|
||||
_refreshVisible();
|
||||
scheduleMicrotask(job.dispose);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,16 +21,15 @@ class FileClipboard extends ChangeNotifier {
|
||||
List<CacheableFile> get files => List.unmodifiable(_files);
|
||||
bool get isEmpty => _files.isEmpty;
|
||||
|
||||
void cut(List<CacheableFile> files) {
|
||||
if (files.isEmpty) return;
|
||||
_operation = FileClipboardOperation.cut;
|
||||
_files = List.of(files);
|
||||
notifyListeners();
|
||||
}
|
||||
void cut(List<CacheableFile> files) =>
|
||||
_set(FileClipboardOperation.cut, files);
|
||||
|
||||
void copy(List<CacheableFile> files) {
|
||||
void copy(List<CacheableFile> files) =>
|
||||
_set(FileClipboardOperation.copy, files);
|
||||
|
||||
void _set(FileClipboardOperation operation, List<CacheableFile> files) {
|
||||
if (files.isEmpty) return;
|
||||
_operation = FileClipboardOperation.copy;
|
||||
_operation = operation;
|
||||
_files = List.of(files);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -8,12 +8,3 @@ String randomHexId({int bytes = 16}) {
|
||||
(_) => random.nextInt(256),
|
||||
).map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
/// Random RFC 4122 version-4 UUID, for ids a server expects in UUID form.
|
||||
String randomUuidV4() {
|
||||
final hex = randomHexId();
|
||||
final variant = (int.parse(hex[16], radix: 16) & 0x3 | 0x8).toRadixString(16);
|
||||
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-'
|
||||
'4${hex.substring(13, 16)}-$variant${hex.substring(17, 20)}-'
|
||||
'${hex.substring(20)}';
|
||||
}
|
||||
@@ -32,25 +32,14 @@ class _LoginCardState extends State<LoginCard> {
|
||||
final _passwordController = TextEditingController();
|
||||
final _passwordFocus = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onControllerChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onControllerChange);
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
_passwordFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onControllerChange() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
String? _required(String? value) =>
|
||||
(value ?? '').trim().isEmpty ? 'Eingabe erforderlich' : null;
|
||||
|
||||
@@ -81,7 +70,10 @@ class _LoginCardState extends State<LoginCard> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context) =>
|
||||
ListenableBuilder(listenable: widget.controller, builder: _buildForm);
|
||||
|
||||
Widget _buildForm(BuildContext context, Widget? _) {
|
||||
final theme = Theme.of(context);
|
||||
final loading = widget.controller.loading;
|
||||
return Form(
|
||||
|
||||
@@ -30,6 +30,12 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
|
||||
final TextEditingController _lastName = TextEditingController();
|
||||
final TextEditingController _phone = TextEditingController();
|
||||
final TextEditingController _note = TextEditingController();
|
||||
late final List<TextEditingController> _textControllers = [
|
||||
_firstName,
|
||||
_lastName,
|
||||
_phone,
|
||||
_note,
|
||||
];
|
||||
final AsyncActionController _submitController = AsyncActionController();
|
||||
|
||||
late Future<void> _init;
|
||||
@@ -46,7 +52,7 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
|
||||
final today = DateUtils.dateOnly(DateTime.now());
|
||||
_absentFrom = today;
|
||||
_absentUntil = today;
|
||||
for (final c in [_firstName, _lastName, _phone, _note]) {
|
||||
for (final c in _textControllers) {
|
||||
c.addListener(_onFieldChanged);
|
||||
}
|
||||
_init = _load();
|
||||
@@ -54,7 +60,7 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in [_firstName, _lastName, _phone, _note]) {
|
||||
for (final c in _textControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
_submitController.dispose();
|
||||
@@ -290,13 +296,11 @@ class _AbsenceReportViewState extends State<AbsenceReportView> {
|
||||
String? _requiredError(TextEditingController controller) =>
|
||||
_submitted && controller.text.trim().isEmpty ? _required : null;
|
||||
|
||||
InputDecoration _decoration(String label, {String? error, String? hint}) =>
|
||||
InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
errorText: error,
|
||||
);
|
||||
InputDecoration _decoration(String label, {String? error}) => InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: label,
|
||||
errorText: error,
|
||||
);
|
||||
}
|
||||
|
||||
/// Terminal success screen shown in place of the form after a report was
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
||||
import '../../../../api/marianumcloud/webdav/queries/list_files/list_files_response.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import 'sort_options.dart';
|
||||
|
||||
typedef FileSort = ({SortOption by, bool ascending, bool foldersToTop});
|
||||
|
||||
/// Selector for `context.select`, so folder views rebuild only when the
|
||||
/// file sort settings change.
|
||||
FileSort fileSortOf(SettingsCubit cubit) {
|
||||
final settings = cubit.state.fileSettings;
|
||||
return (
|
||||
by: settings.sortBy,
|
||||
ascending: settings.ascending,
|
||||
foldersToTop: settings.sortFoldersToTop,
|
||||
);
|
||||
}
|
||||
|
||||
extension FileSortListing on ListFilesResponse {
|
||||
List<CacheableFile> sortedBy(FileSort sort) => sortBy(
|
||||
sortOption: sort.by,
|
||||
foldersToTop: sort.foldersToTop,
|
||||
reversed: sort.ascending,
|
||||
);
|
||||
}
|
||||
@@ -11,12 +11,7 @@ IconData iconForFile(CacheableFile file) {
|
||||
final byMime = _iconForMime(mime);
|
||||
if (byMime != null) return byMime;
|
||||
}
|
||||
final ext = _extensionOf(file.name);
|
||||
if (ext != null) {
|
||||
final byExt = _extensionIcons[ext];
|
||||
if (byExt != null) return byExt;
|
||||
}
|
||||
return Icons.insert_drive_file_outlined;
|
||||
return iconForFileName(file.name);
|
||||
}
|
||||
|
||||
/// Icon for a bare file name (no [CacheableFile] / MIME available), e.g. in the
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Paths relative to the WebDAV root, as used by `CacheableFile.path`: no
|
||||
// leading slash, folders end with '/', the root folder is ''.
|
||||
|
||||
final RegExp _edgeSlashes = RegExp(r'^/+|/+$');
|
||||
|
||||
List<String> webdavSegmentsOf(String path) {
|
||||
final stripped = path.replaceAll(_edgeSlashes, '');
|
||||
return stripped.isEmpty ? [] : stripped.split('/');
|
||||
}
|
||||
|
||||
List<String> webdavParentSegmentsOf(String path) {
|
||||
final segments = webdavSegmentsOf(path);
|
||||
if (segments.isNotEmpty) segments.removeLast();
|
||||
return segments;
|
||||
}
|
||||
|
||||
String webdavFolderPath(List<String> segments) =>
|
||||
segments.isEmpty ? '' : '${segments.join('/')}/';
|
||||
|
||||
String webdavJoin(String folder, String name, {required bool isDirectory}) =>
|
||||
isDirectory ? '$folder$name/' : '$folder$name';
|
||||
|
||||
/// Folder key used by `ListFilesCache` and `FilesBloc`: no leading or
|
||||
/// trailing slash, root is '/'.
|
||||
String listFilesCacheKey(List<String> segments) =>
|
||||
segments.isEmpty ? '/' : segments.join('/');
|
||||
@@ -2,20 +2,19 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
||||
import '../../../api/marianumcloud/webdav/queries/list_files/list_files_response.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/files/bloc/files_bloc.dart';
|
||||
import '../../../state/app/modules/files/bloc/files_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../utils/cache_invalidation_bus.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import 'data/sort_options.dart';
|
||||
import 'files_upload_dialog.dart';
|
||||
import 'data/file_sort.dart';
|
||||
import 'data/webdav_path.dart';
|
||||
import 'search/files_search_delegate.dart';
|
||||
import 'widgets/add_file_menu.dart';
|
||||
import 'widgets/clipboard_banner.dart';
|
||||
@@ -44,9 +43,6 @@ class _FilesView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FilesViewState extends State<_FilesView> {
|
||||
late final SettingsCubit settings;
|
||||
late SortOption currentSort;
|
||||
late bool currentSortDirection;
|
||||
late final StreamSubscription<String> _invalidationSub;
|
||||
|
||||
// The list builder also runs for loading flips and parent rebuilds; only a
|
||||
@@ -54,32 +50,16 @@ class _FilesViewState extends State<_FilesView> {
|
||||
Object? _sortedKey;
|
||||
List<CacheableFile> _sortedFiles = const [];
|
||||
|
||||
List<CacheableFile> _sorted(ListFilesResponse listing, bool foldersToTop) {
|
||||
final key = (listing, currentSort, currentSortDirection, foldersToTop);
|
||||
List<CacheableFile> _sorted(ListFilesResponse listing, FileSort sort) {
|
||||
final key = (listing, sort);
|
||||
if (key == _sortedKey) return _sortedFiles;
|
||||
_sortedKey = key;
|
||||
return _sortedFiles = listing.sortBy(
|
||||
sortOption: currentSort,
|
||||
foldersToTop: foldersToTop,
|
||||
reversed: currentSortDirection,
|
||||
);
|
||||
return _sortedFiles = listing.sortedBy(sort);
|
||||
}
|
||||
|
||||
// Cache key in FilesBloc's pathString format: '/' for root, otherwise
|
||||
// segments joined without leading/trailing slash.
|
||||
String get _myPathString => widget.path.isEmpty ? '/' : widget.path.join('/');
|
||||
|
||||
// Relative folder path matching the WebDAV format used by `CacheableFile.path`
|
||||
// (no leading slash; trailing slash for non-root). Empty string means root.
|
||||
String get _currentFolderPath =>
|
||||
widget.path.isEmpty ? '' : '${widget.path.join('/')}/';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
settings = context.read<SettingsCubit>();
|
||||
currentSort = settings.val().fileSettings.sortBy;
|
||||
currentSortDirection = settings.val().fileSettings.ascending;
|
||||
_invalidationSub = CacheInvalidationBus.listFilesStream.listen(
|
||||
_onInvalidation,
|
||||
);
|
||||
@@ -87,7 +67,7 @@ class _FilesViewState extends State<_FilesView> {
|
||||
|
||||
void _onInvalidation(String invalidatedPath) {
|
||||
if (!mounted) return;
|
||||
if (invalidatedPath != _myPathString) return;
|
||||
if (invalidatedPath != listFilesCacheKey(widget.path)) return;
|
||||
context.read<FilesBloc>().refresh();
|
||||
}
|
||||
|
||||
@@ -101,14 +81,11 @@ class _FilesViewState extends State<_FilesView> {
|
||||
if (paths == null) return;
|
||||
final bloc = context.read<FilesBloc>();
|
||||
unawaited(
|
||||
pushScreen(
|
||||
AppRoutes.openFilesUpload(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: paths,
|
||||
remotePath: widget.path.join('/'),
|
||||
onUploadFinished: (_) => bloc.refresh(),
|
||||
),
|
||||
filePaths: paths,
|
||||
remotePath: widget.path.join('/'),
|
||||
onUploadFinished: (_) => bloc.refresh(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -118,29 +95,12 @@ class _FilesViewState extends State<_FilesView> {
|
||||
final bloc = context.read<FilesBloc>();
|
||||
// Selected here, not in the LoadableStateConsumer child: that closure runs
|
||||
// during the consumer's build, where this context may not subscribe.
|
||||
final foldersToTop = context.select(
|
||||
(SettingsCubit c) => c.state.fileSettings.sortFoldersToTop,
|
||||
);
|
||||
final sort = context.select(fileSortOf);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.path.isNotEmpty ? widget.path.last : 'Dateien'),
|
||||
actions: [
|
||||
FilesSortActions(
|
||||
currentSort: currentSort,
|
||||
ascending: currentSortDirection,
|
||||
onDirectionChanged: (e) {
|
||||
setState(() {
|
||||
currentSortDirection = e;
|
||||
settings.val(write: true).fileSettings.ascending = e;
|
||||
});
|
||||
},
|
||||
onSortChanged: (e) {
|
||||
setState(() {
|
||||
currentSort = e;
|
||||
settings.val(write: true).fileSettings.sortBy = e;
|
||||
});
|
||||
},
|
||||
),
|
||||
const FilesSortActions(),
|
||||
IconButton(
|
||||
tooltip: 'Suchen',
|
||||
icon: const Icon(Icons.search),
|
||||
@@ -162,7 +122,7 @@ class _FilesViewState extends State<_FilesView> {
|
||||
body: Column(
|
||||
children: [
|
||||
ClipboardBanner(
|
||||
currentFolder: _currentFolderPath,
|
||||
currentFolder: webdavFolderPath(widget.path),
|
||||
onPasteDone: bloc.refresh,
|
||||
),
|
||||
Expanded(
|
||||
@@ -176,7 +136,7 @@ class _FilesViewState extends State<_FilesView> {
|
||||
text: 'Der Ordner ist leer',
|
||||
);
|
||||
}
|
||||
final files = _sorted(listing, foldersToTop);
|
||||
final files = _sorted(listing, sort);
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: files.length,
|
||||
|
||||
@@ -28,13 +28,15 @@ class FilesUploadDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class UploadableFile {
|
||||
TextEditingController fileNameController = TextEditingController();
|
||||
String filePath;
|
||||
String fileName;
|
||||
final String filePath;
|
||||
final TextEditingController fileNameController;
|
||||
double? _uploadProgress;
|
||||
bool isConflicting = false;
|
||||
|
||||
UploadableFile(this.filePath, this.fileName);
|
||||
UploadableFile(this.filePath, String fileName)
|
||||
: fileNameController = TextEditingController(text: fileName);
|
||||
|
||||
String get fileName => fileNameController.text;
|
||||
}
|
||||
|
||||
class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
@@ -47,10 +49,22 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_uploadableFiles = widget.filePaths.map((filePath) {
|
||||
var fileName = filePath.split(Platform.pathSeparator).last;
|
||||
return UploadableFile(filePath, fileName);
|
||||
}).toList();
|
||||
_uploadableFiles = widget.filePaths
|
||||
.map(
|
||||
(filePath) => UploadableFile(
|
||||
filePath,
|
||||
filePath.split(Platform.pathSeparator).last,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final file in _uploadableFiles) {
|
||||
file.fileNameController.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _resetProgress() {
|
||||
@@ -69,6 +83,10 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
void _clearConflict(UploadableFile file) {
|
||||
if (file.isConflicting) setState(() => file.isConflicting = false);
|
||||
}
|
||||
|
||||
Future<void> uploadFiles({bool override = false}) async {
|
||||
setState(() {
|
||||
_isUploading = true;
|
||||
@@ -82,36 +100,39 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
if (!mounted) return;
|
||||
|
||||
if (!override) {
|
||||
List<dynamic> result;
|
||||
final List<String> existingPaths;
|
||||
try {
|
||||
result = (await webdavClient.propfind(
|
||||
final result = await webdavClient.propfind(
|
||||
PathUri.parse(widget.remotePath),
|
||||
)).responses;
|
||||
);
|
||||
existingPaths = [
|
||||
for (final response in result.responses)
|
||||
Uri.decodeComponent(response.href!),
|
||||
];
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
_showUploadError('Verbindung fehlgeschlagen: $e');
|
||||
return;
|
||||
}
|
||||
final conflictingFiles = _uploadableFiles.where((file) {
|
||||
final fileName = file.fileName;
|
||||
return result.any(
|
||||
(element) => Uri.decodeComponent(
|
||||
(element as WebDavResponse).href!,
|
||||
).endsWith('/$fileName'),
|
||||
);
|
||||
}).toList();
|
||||
final conflictingFiles = _uploadableFiles
|
||||
.where(
|
||||
(file) => existingPaths.any(
|
||||
(path) => path.endsWith('/${file.fileName}'),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (conflictingFiles.isNotEmpty) {
|
||||
if (!mounted) return;
|
||||
final replaceFiles = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
title: const Text('Konflikt', textAlign: TextAlign.center),
|
||||
content: conflictingFiles.length == 1
|
||||
? Text(
|
||||
'Eine Datei mit dem Namen "${conflictingFiles.map((e) => e.fileName).first}" existiert bereits.',
|
||||
'Eine Datei mit dem Namen "${conflictingFiles.first.fileName}" existiert bereits.',
|
||||
textAlign: TextAlign.left,
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
@@ -122,27 +143,20 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('Bearbeiten', textAlign: TextAlign.center),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ConfirmDialog(
|
||||
title: 'Bestätigen?',
|
||||
content:
|
||||
'Bist du sicher, dass du ${conflictingFiles.length} Dateien überschreiben möchtest?',
|
||||
onConfirm: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
confirmButton: 'Ja',
|
||||
cancelButton: 'Nein',
|
||||
),
|
||||
);
|
||||
},
|
||||
// ConfirmDialog closes itself first, so this pop closes the
|
||||
// conflict dialog.
|
||||
onPressed: () => ConfirmDialog(
|
||||
title: 'Bestätigen?',
|
||||
content:
|
||||
'Bist du sicher, dass du ${conflictingFiles.length} Dateien überschreiben möchtest?',
|
||||
onConfirm: () => Navigator.pop(dialogContext, true),
|
||||
confirmButton: 'Ja',
|
||||
cancelButton: 'Nein',
|
||||
).asDialog(dialogContext),
|
||||
child: const Text('Überschreiben', textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
@@ -163,9 +177,9 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
}
|
||||
|
||||
var uploadetFilePaths = <String>[];
|
||||
for (var file in _uploadableFiles) {
|
||||
for (final (fileIndex, file) in _uploadableFiles.indexed) {
|
||||
var fileName = file.fileName;
|
||||
var filePath = file.filePath;
|
||||
final filePath = file.filePath;
|
||||
|
||||
if (widget.uniqueNames) {
|
||||
final unique = DateTime.now().microsecondsSinceEpoch.toRadixString(36);
|
||||
@@ -177,7 +191,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
|
||||
setState(() {
|
||||
_infoText =
|
||||
'${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}';
|
||||
'${fileIndex + 1}/${_uploadableFiles.length}';
|
||||
});
|
||||
|
||||
// A vanished source file would otherwise surface as a cryptic
|
||||
@@ -193,7 +207,6 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
}
|
||||
|
||||
final HttpClientResponse uploadTask;
|
||||
final fileIndex = _uploadableFiles.indexOf(file);
|
||||
var lastPercent = -1;
|
||||
try {
|
||||
uploadTask = await webdavClient.putFile(
|
||||
@@ -253,12 +266,6 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
itemCount: _uploadableFiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final currentFile = _uploadableFiles[index];
|
||||
// Only sync when it differs: assigning text resets selection
|
||||
// and notifies the field on every (progress) rebuild.
|
||||
if (currentFile.fileNameController.text !=
|
||||
currentFile.fileName) {
|
||||
currentFile.fileNameController.text = currentFile.fileName;
|
||||
}
|
||||
return ListTile(
|
||||
title: TextField(
|
||||
readOnly: _isUploading,
|
||||
@@ -273,24 +280,11 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onChanged: (input) {
|
||||
currentFile.fileName = input;
|
||||
},
|
||||
onTapOutside: (PointerDownEvent event) {
|
||||
FocusBehaviour.textFieldTapOutside(context);
|
||||
if (currentFile.isConflicting) {
|
||||
setState(() {
|
||||
currentFile.isConflicting = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
onEditingComplete: () {
|
||||
if (currentFile.isConflicting) {
|
||||
setState(() {
|
||||
currentFile.isConflicting = false;
|
||||
});
|
||||
}
|
||||
_clearConflict(currentFile);
|
||||
},
|
||||
onEditingComplete: () => _clearConflict(currentFile),
|
||||
),
|
||||
subtitle:
|
||||
_isUploading && (currentFile._uploadProgress ?? 0) < 1
|
||||
@@ -301,21 +295,23 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
),
|
||||
)
|
||||
: null,
|
||||
trailing: Container(
|
||||
trailing: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
padding: EdgeInsets.zero,
|
||||
child: IconButton(
|
||||
tooltip: 'Datei entfernen',
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
if (!_isUploading) {
|
||||
if (_uploadableFiles.length - 1 <= 0) {
|
||||
if (_uploadableFiles.length == 1) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
setState(() {
|
||||
_uploadableFiles.removeAt(index);
|
||||
});
|
||||
final removed = _uploadableFiles[index];
|
||||
setState(() => _uploadableFiles.removeAt(index));
|
||||
// Its TextField is still mounted until the rebuild.
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => removed.fileNameController.dispose(),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.delete_outlined),
|
||||
@@ -342,7 +338,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
),
|
||||
const Expanded(child: SizedBox.shrink()),
|
||||
const Spacer(),
|
||||
Visibility(
|
||||
visible: _isUploading,
|
||||
replacement: TextButton(
|
||||
|
||||
Loaded 100 of 189 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user