Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54ba04a7bd | |||
| 9b5a70b285 | |||
| 4f796dac2e | |||
| db9c3386f1 | |||
| bee5c02a4f |
@@ -1,4 +1,4 @@
|
||||
class ApiError {
|
||||
class ApiError implements Exception {
|
||||
String message;
|
||||
|
||||
ApiError(this.message);
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../requestCache.dart';
|
||||
import 'getHolidays.dart';
|
||||
import 'getHolidaysResponse.dart';
|
||||
|
||||
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
|
||||
GetHolidaysCache({void Function(GetHolidaysResponse)? onUpdate, bool? renew}) : super(RequestCache.cacheDay, onUpdate, renew: renew) {
|
||||
class GetHolidaysCache extends SimpleCache<GetHolidaysResponse> {
|
||||
GetHolidaysCache({super.onUpdate, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheDay,
|
||||
loader: () => GetHolidays().query(),
|
||||
fromJson: (json) => GetHolidaysResponse(
|
||||
(json['data'] as List)
|
||||
.map((i) => GetHolidaysResponseObject.fromJson(i as Map<String, dynamic>))
|
||||
.toList(),
|
||||
),
|
||||
) {
|
||||
start('state-holidays');
|
||||
}
|
||||
|
||||
@override
|
||||
GetHolidaysResponse onLocalData(String json) {
|
||||
List<dynamic> parsedListJson = jsonDecode(json)['data'];
|
||||
return GetHolidaysResponse(
|
||||
List<GetHolidaysResponseObject>.from(
|
||||
parsedListJson.map<GetHolidaysResponseObject>(
|
||||
(i) => GetHolidaysResponseObject.fromJson(i as Map<String, dynamic>)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetHolidaysResponse> onLoad() => GetHolidays().query();
|
||||
}
|
||||
|
||||
@@ -3,30 +3,25 @@ import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/accountData.dart';
|
||||
import '../../../model/endpointData.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'autocompleteResponse.dart';
|
||||
|
||||
class AutocompleteApi {
|
||||
Future<AutocompleteResponse> find(String query) async {
|
||||
var getParameters = <String, dynamic>{
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'core/autocomplete/get',
|
||||
queryParameters: {
|
||||
'search': query,
|
||||
'itemType': ' ',
|
||||
'itemId': ' ',
|
||||
'shareTypes[]': ['0'],
|
||||
'limit': '10',
|
||||
};
|
||||
|
||||
var headers = <String, String>{};
|
||||
headers.putIfAbsent('Accept', () => 'application/json');
|
||||
headers.putIfAbsent('OCS-APIRequest', () => 'true');
|
||||
|
||||
var endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/core/autocomplete/get', getParameters);
|
||||
|
||||
var response = await http.get(endpoint, headers: headers);
|
||||
if(response.statusCode != HttpStatus.ok) throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
|
||||
var result = response.body;
|
||||
return AutocompleteResponse.fromJson(jsonDecode(result)['ocs']);
|
||||
},
|
||||
);
|
||||
final response = await http.get(endpoint, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
return AutocompleteResponse.fromJson(jsonDecode(response.body)['ocs']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,20 +2,17 @@ import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/accountData.dart';
|
||||
import '../../../model/endpointData.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
import 'fileSharingApiParams.dart';
|
||||
|
||||
class FileSharingApi {
|
||||
Future<void> share(FileSharingApiParams query) async {
|
||||
var headers = <String, String>{};
|
||||
headers.putIfAbsent('Accept', () => 'application/json');
|
||||
headers.putIfAbsent('OCS-APIRequest', () => 'true');
|
||||
|
||||
var endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/apps/files_sharing/api/v1/shares', query.toJson().map((key, value) => MapEntry(key, value.toString())));
|
||||
var response = await http.post(endpoint, headers: headers);
|
||||
|
||||
if(response.statusCode != HttpStatus.ok) {
|
||||
final endpoint = NextcloudOcs.uri(
|
||||
'apps/files_sharing/api/v1/shares',
|
||||
queryParameters: query.toJson(),
|
||||
);
|
||||
final response = await http.post(endpoint, headers: NextcloudOcs.headers());
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
throw Exception('Api call failed with ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import '../../model/account_data.dart';
|
||||
import '../../model/endpoint_data.dart';
|
||||
|
||||
/// Shared helpers for Nextcloud OCS v2 endpoints.
|
||||
///
|
||||
/// Three call sites previously duplicated the same header dictionary and the
|
||||
/// same URI scaffolding (TalkApi, AutocompleteApi, FileSharingApi). Anything
|
||||
/// that talks to `https://<domain>/<base>/ocs/v2.php/...` should go through
|
||||
/// these two helpers so additions like a new header or a different auth
|
||||
/// scheme only need to change here.
|
||||
class NextcloudOcs {
|
||||
NextcloudOcs._();
|
||||
|
||||
/// The standard OCS request header set: JSON accept, OCS API marker,
|
||||
/// HTTP Basic auth from the active [AccountData].
|
||||
static Map<String, String> headers() => {
|
||||
'Accept': 'application/json',
|
||||
'OCS-APIRequest': 'true',
|
||||
'Authorization': AccountData().getBasicAuthHeader(),
|
||||
};
|
||||
|
||||
/// Builds an OCS URI by appending [pathSuffix] under `/ocs/v2.php/` of
|
||||
/// the configured Nextcloud endpoint. Query parameters are converted to
|
||||
/// strings (Uri rejects non-string values).
|
||||
static Uri uri(String pathSuffix, {Map<String, dynamic>? queryParameters}) {
|
||||
final endpoint = EndpointData().nextcloud();
|
||||
return Uri.https(
|
||||
endpoint.domain,
|
||||
'${endpoint.path}/ocs/v2.php/$pathSuffix',
|
||||
queryParameters?.map((key, value) => MapEntry(key, value.toString())),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../apiParams.dart';
|
||||
import '../../../apiResponse.dart';
|
||||
import '../talkApi.dart';
|
||||
|
||||
/// Small POST/DELETE-only Talk endpoints that have no response payload.
|
||||
/// Each class extends [TalkApi] with `assemble` returning `null`. They share
|
||||
/// no state — they're collected here purely to avoid eight near-empty files.
|
||||
|
||||
class SetFavorite extends TalkApi {
|
||||
final String chatToken;
|
||||
final bool favoriteState;
|
||||
|
||||
SetFavorite(this.chatToken, this.favoriteState) : super('v4/room/$chatToken/favorite', null);
|
||||
|
||||
@override
|
||||
ApiResponse? assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) =>
|
||||
favoriteState ? http.post(uri, headers: headers) : http.delete(uri, headers: headers);
|
||||
}
|
||||
|
||||
class LeaveRoom extends TalkApi {
|
||||
final String chatToken;
|
||||
|
||||
LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null);
|
||||
|
||||
@override
|
||||
ApiResponse? assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) =>
|
||||
http.delete(uri, headers: headers);
|
||||
}
|
||||
|
||||
class DeleteMessage extends TalkApi {
|
||||
final String chatToken;
|
||||
final int messageId;
|
||||
|
||||
DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null);
|
||||
|
||||
@override
|
||||
ApiResponse? assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<http.Response> request(Uri uri, ApiParams? body, Map<String, String>? headers) =>
|
||||
http.delete(uri, headers: headers);
|
||||
}
|
||||
@@ -1,28 +1,26 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getChat.dart';
|
||||
import 'getChatParams.dart';
|
||||
import 'getChatResponse.dart';
|
||||
|
||||
class GetChatCache extends RequestCache<GetChatResponse> {
|
||||
String chatToken;
|
||||
|
||||
GetChatCache({required void Function(GetChatResponse) onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
|
||||
start('nc-chat-$chatToken');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetChatResponse> onLoad() => GetChat(
|
||||
class GetChatCache extends SimpleCache<GetChatResponse> {
|
||||
GetChatCache({
|
||||
required void Function(GetChatResponse) onUpdate,
|
||||
super.onError,
|
||||
required String chatToken,
|
||||
}) : super(
|
||||
cacheTime: RequestCache.cacheNothing,
|
||||
loader: () => GetChat(
|
||||
chatToken,
|
||||
GetChatParams(
|
||||
lookIntoFuture: GetChatParamsSwitch.off,
|
||||
setReadMarker: GetChatParamsSwitch.on,
|
||||
limit: 200,
|
||||
)
|
||||
).run();
|
||||
|
||||
@override
|
||||
GetChatResponse onLocalData(String json) => GetChatResponse.fromJson(jsonDecode(json));
|
||||
|
||||
),
|
||||
).run(),
|
||||
fromJson: GetChatResponse.fromJson,
|
||||
onUpdate: onUpdate,
|
||||
) {
|
||||
start('nc-chat-$chatToken');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../../../apiParams.dart';
|
||||
import '../talkApi.dart';
|
||||
|
||||
class DeleteMessage extends TalkApi {
|
||||
String chatToken;
|
||||
int messageId;
|
||||
DeleteMessage(this.chatToken, this.messageId) : super('v1/chat/$chatToken/$messageId', null);
|
||||
|
||||
@override
|
||||
assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<Response>? request(Uri uri, ApiParams? body, Map<String, String>? headers) => http.delete(uri, headers: headers);
|
||||
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getParticipants.dart';
|
||||
import 'getParticipantsResponse.dart';
|
||||
|
||||
class GetParticipantsCache extends RequestCache<GetParticipantsResponse> {
|
||||
String chatToken;
|
||||
|
||||
GetParticipantsCache({required void Function(GetParticipantsResponse) onUpdate, required this.chatToken}) : super(RequestCache.cacheNothing, onUpdate) {
|
||||
class GetParticipantsCache extends SimpleCache<GetParticipantsResponse> {
|
||||
GetParticipantsCache({
|
||||
required void Function(GetParticipantsResponse) onUpdate,
|
||||
required String chatToken,
|
||||
}) : super(
|
||||
cacheTime: RequestCache.cacheNothing,
|
||||
loader: () => GetParticipants(chatToken).run(),
|
||||
fromJson: GetParticipantsResponse.fromJson,
|
||||
onUpdate: onUpdate,
|
||||
) {
|
||||
start('nc-chat-participants-$chatToken');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetParticipantsResponse> onLoad() => GetParticipants(
|
||||
chatToken,
|
||||
).run();
|
||||
|
||||
@override
|
||||
GetParticipantsResponse onLocalData(String json) => GetParticipantsResponse.fromJson(jsonDecode(json));
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../talkApi.dart';
|
||||
class LeaveRoom extends TalkApi {
|
||||
String chatToken;
|
||||
|
||||
LeaveRoom(this.chatToken) : super('v4/room/$chatToken/participants/self', null);
|
||||
|
||||
@override
|
||||
assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) => http.delete(uri, headers: headers);
|
||||
}
|
||||
@@ -1,23 +1,15 @@
|
||||
import 'dart:convert';
|
||||
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getRoom.dart';
|
||||
import 'getRoomParams.dart';
|
||||
import 'getRoomResponse.dart';
|
||||
|
||||
class GetRoomCache extends RequestCache<GetRoomResponse> {
|
||||
GetRoomCache({void Function(GetRoomResponse)? onUpdate, bool? renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
|
||||
class GetRoomCache extends SimpleCache<GetRoomResponse> {
|
||||
GetRoomCache({super.onUpdate, super.onError, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheMinute,
|
||||
loader: () => GetRoom(GetRoomParams(includeStatus: true)).run(),
|
||||
fromJson: GetRoomResponse.fromJson,
|
||||
) {
|
||||
start('nc-rooms');
|
||||
}
|
||||
|
||||
@override
|
||||
GetRoomResponse onLocalData(String json) => GetRoomResponse.fromJson(jsonDecode(json));
|
||||
|
||||
@override
|
||||
Future<GetRoomResponse> onLoad() => GetRoom(
|
||||
GetRoomParams(
|
||||
includeStatus: true,
|
||||
)
|
||||
).run();
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import '../../../apiResponse.dart';
|
||||
|
||||
class SendMessageResponse extends ApiResponse {
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../talkApi.dart';
|
||||
|
||||
class SetFavorite extends TalkApi {
|
||||
String chatToken;
|
||||
bool favoriteState;
|
||||
|
||||
SetFavorite(this.chatToken, this.favoriteState) : super('v4/room/$chatToken/favorite', null);
|
||||
|
||||
@override
|
||||
assemble(String raw) => null;
|
||||
|
||||
@override
|
||||
Future<Response> request(Uri uri, Object? body, Map<String, String>? headers) {
|
||||
if(favoriteState) {
|
||||
return http.post(uri, headers: headers);
|
||||
} else {
|
||||
return http.delete(uri, headers: headers);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,12 +2,11 @@ import 'dart:developer';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../model/accountData.dart';
|
||||
import '../../../model/endpointData.dart';
|
||||
import '../../apiError.dart';
|
||||
import '../../apiParams.dart';
|
||||
import '../../apiRequest.dart';
|
||||
import '../../apiResponse.dart';
|
||||
import '../nextcloud_ocs.dart';
|
||||
|
||||
enum TalkApiMethod {
|
||||
get,
|
||||
@@ -19,7 +18,7 @@ enum TalkApiMethod {
|
||||
abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
||||
String path;
|
||||
ApiParams? body;
|
||||
Map<String, String>? headers = {};
|
||||
Map<String, String>? headers;
|
||||
Map<String, dynamic>? getParameters;
|
||||
|
||||
http.Response? response;
|
||||
@@ -30,35 +29,28 @@ abstract class TalkApi<T extends ApiResponse?> extends ApiRequest {
|
||||
T assemble(String raw);
|
||||
|
||||
Future<T> run() async {
|
||||
getParameters?.forEach((key, value) {
|
||||
getParameters?.update(key, (value) => value.toString());
|
||||
});
|
||||
|
||||
var endpoint = Uri.https('${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().domain}', '${EndpointData().nextcloud().path}/ocs/v2.php/apps/spreed/api/$path', getParameters);
|
||||
|
||||
headers ??= {};
|
||||
headers?.putIfAbsent('Accept', () => 'application/json');
|
||||
headers?.putIfAbsent('OCS-APIRequest', () => 'true');
|
||||
final endpoint = NextcloudOcs.uri('apps/spreed/api/$path', queryParameters: getParameters);
|
||||
final mergedHeaders = {...NextcloudOcs.headers(), ...?headers};
|
||||
|
||||
http.Response? data;
|
||||
|
||||
try {
|
||||
data = await request(endpoint, body, headers);
|
||||
if(data == null) throw Exception('No response Data');
|
||||
if(data.statusCode >= 400 || data.statusCode < 200) throw Exception("Response status code '${data.statusCode}' might indicate an error");
|
||||
} catch(e) {
|
||||
data = await request(endpoint, body, mergedHeaders);
|
||||
if (data == null) throw Exception('No response Data');
|
||||
if (data.statusCode >= 400 || data.statusCode < 200) {
|
||||
throw Exception("Response status code '${data.statusCode}' might indicate an error");
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
throw ApiError('Request $endpoint could not be dispatched: ${e.toString()}');
|
||||
}
|
||||
//dynamic jsonData = jsonDecode(data.body);
|
||||
|
||||
T assembled;
|
||||
try {
|
||||
assembled = assemble(data.body);
|
||||
final assembled = assemble(data.body);
|
||||
assembled?.headers = data.headers;
|
||||
return assembled;
|
||||
} catch (e) {
|
||||
var message = 'Error assembling Talk API ${T.toString()} message: ${e.toString()} response with request body: $body and request headers: ${headers.toString()}';
|
||||
final message = 'Error assembling Talk API ${T.toString()} message: ${e.toString()}'
|
||||
' response with request body: $body and request headers: $mergedHeaders';
|
||||
log(message);
|
||||
throw Exception(message);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../getPoll/getPollStateResponse.dart';
|
||||
import '../talkApi.dart';
|
||||
import 'votePollParams.dart';
|
||||
|
||||
@Deprecated('VotePoll is broken')
|
||||
class VotePoll extends TalkApi {
|
||||
String token;
|
||||
int pollId;
|
||||
VotePoll({required this.token, required this.pollId, required VotePollParams params}) : super('v1/poll/$token/$pollId', params);
|
||||
|
||||
@override
|
||||
GetPollStateResponse assemble(String raw) => GetPollStateResponse.fromJson(jsonDecode(raw)['ocs']);
|
||||
|
||||
@override
|
||||
Future<Response>? request(Uri uri, Object? body, Map<String, String>? headers) {
|
||||
if(body is VotePollParams) {
|
||||
return http.post(uri, headers: headers, body: body.toJson().toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../apiParams.dart';
|
||||
|
||||
part 'votePollParams.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@Deprecated('VotePoll is broken')
|
||||
class VotePollParams extends ApiParams {
|
||||
List<int> optionIds;
|
||||
|
||||
VotePollParams({required this.optionIds});
|
||||
factory VotePollParams.fromJson(Map<String, dynamic> json) => _$VotePollParamsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$VotePollParamsToJson(this);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'votePollParams.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
VotePollParams _$VotePollParamsFromJson(Map<String, dynamic> json) =>
|
||||
VotePollParams(
|
||||
optionIds: (json['optionIds'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$VotePollParamsToJson(VotePollParams instance) =>
|
||||
<String, dynamic>{'optionIds': instance.optionIds};
|
||||
@@ -11,9 +11,22 @@ class ListFiles extends WebdavApi<ListFilesParams> {
|
||||
|
||||
ListFiles(this.params) : super(params);
|
||||
|
||||
// The Nextcloud root listing is significantly slower than subdirectories on
|
||||
// our instance, so it gets a much longer ceiling. Subfolders fall back to a
|
||||
// tighter timeout to keep the UI responsive.
|
||||
static const Duration _rootTimeout = Duration(minutes: 3);
|
||||
static const Duration _subfolderTimeout = Duration(seconds: 30);
|
||||
|
||||
bool get _isRoot {
|
||||
final p = params.path.replaceAll('/', '').trim();
|
||||
return p.isEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ListFilesResponse> run() async {
|
||||
var davFiles = (await (await WebdavApi.webdav).propfind(PathUri.parse(params.path))).toWebDavFiles();
|
||||
final webdav = await WebdavApi.webdav;
|
||||
final timeout = _isRoot ? _rootTimeout : _subfolderTimeout;
|
||||
final davFiles = (await webdav.propfind(PathUri.parse(params.path)).timeout(timeout)).toWebDavFiles();
|
||||
var files = davFiles.map(CacheableFile.fromDavFile).toSet();
|
||||
|
||||
// webdav handles subdirectories wrong, this is a fix
|
||||
|
||||
@@ -6,22 +6,20 @@ import 'listFiles.dart';
|
||||
import 'listFilesParams.dart';
|
||||
import 'listFilesResponse.dart';
|
||||
|
||||
class ListFilesCache extends RequestCache<ListFilesResponse> {
|
||||
String path;
|
||||
|
||||
ListFilesCache({required void Function(ListFilesResponse) onUpdate, required this.path}) : super(RequestCache.cacheNothing, onUpdate) {
|
||||
var bytes = utf8.encode('MarianumMobile-$path');
|
||||
var cacheName = md5.convert(bytes).toString();
|
||||
class ListFilesCache extends SimpleCache<ListFilesResponse> {
|
||||
ListFilesCache({
|
||||
required void Function(ListFilesResponse) onUpdate,
|
||||
super.onCacheData,
|
||||
super.onNetworkData,
|
||||
super.onError,
|
||||
required String path,
|
||||
}) : super(
|
||||
cacheTime: RequestCache.cacheNothing,
|
||||
loader: () => ListFiles(ListFilesParams(path)).run(),
|
||||
fromJson: ListFilesResponse.fromJson,
|
||||
onUpdate: onUpdate,
|
||||
) {
|
||||
final cacheName = md5.convert(utf8.encode('MarianumMobile-$path')).toString();
|
||||
start('wd-folder-$cacheName');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ListFilesResponse> onLoad() async {
|
||||
var data = await ListFiles(ListFilesParams(path)).run();
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
ListFilesResponse onLocalData(String json) => ListFilesResponse.fromJson(jsonDecode(json));
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
|
||||
import '../../../model/accountData.dart';
|
||||
import '../../../model/endpointData.dart';
|
||||
import '../../../model/account_data.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../apiRequest.dart';
|
||||
import '../../apiResponse.dart';
|
||||
|
||||
@@ -15,9 +15,11 @@ abstract class WebdavApi<T> extends ApiRequest {
|
||||
Future<ApiResponse> run();
|
||||
|
||||
static Future<WebDavClient> webdav = establishWebdavConnection();
|
||||
static Future<String> webdavConnectString = buildWebdavConnectString();
|
||||
|
||||
static Future<WebDavClient> establishWebdavConnection() async => NextcloudClient(Uri.parse('https://${EndpointData().nextcloud().full()}'), password: AccountData().getPassword(), loginName: AccountData().getUsername()).webdav;
|
||||
|
||||
static Future<String> buildWebdavConnectString() async => 'https://${AccountData().buildHttpAuthString()}@${EndpointData().nextcloud().full()}/remote.php/dav/files/${AccountData().getUsername()}/';
|
||||
/// Builds the WebDAV download URL without embedded credentials. Callers must
|
||||
/// authenticate via the [AccountData.authHeaders] header instead.
|
||||
static String buildWebdavUrl() =>
|
||||
'https://${EndpointData().nextcloud().full()}/remote.php/dav/files/${AccountData().getUsername()}/';
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import '../../../requestCache.dart';
|
||||
import 'getBreakers.dart';
|
||||
import 'getBreakersResponse.dart';
|
||||
|
||||
|
||||
class GetBreakersCache extends RequestCache<GetBreakersResponse> {
|
||||
GetBreakersCache({void Function(GetBreakersResponse)? onUpdate, bool? renew}) : super(RequestCache.cacheMinute, onUpdate, renew: renew) {
|
||||
class GetBreakersCache extends SimpleCache<GetBreakersResponse> {
|
||||
GetBreakersCache({super.onUpdate, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheMinute,
|
||||
loader: () => GetBreakers().run(),
|
||||
fromJson: GetBreakersResponse.fromJson,
|
||||
) {
|
||||
start('breakers');
|
||||
}
|
||||
|
||||
@override
|
||||
GetBreakersResponse onLocalData(String json) => GetBreakersResponse.fromJson(jsonDecode(json));
|
||||
|
||||
@override
|
||||
Future<GetBreakersResponse> onLoad() => GetBreakers().run();
|
||||
}
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getCustomTimetableEvent.dart';
|
||||
import 'getCustomTimetableEventParams.dart';
|
||||
import 'getCustomTimetableEventResponse.dart';
|
||||
|
||||
class GetCustomTimetableEventCache extends RequestCache<GetCustomTimetableEventResponse> {
|
||||
GetCustomTimetableEventParams params;
|
||||
|
||||
class GetCustomTimetableEventCache extends SimpleCache<GetCustomTimetableEventResponse> {
|
||||
GetCustomTimetableEventCache(
|
||||
this.params, {
|
||||
void Function(GetCustomTimetableEventResponse)? onUpdate,
|
||||
void Function(Exception)? onError,
|
||||
bool? renew,
|
||||
GetCustomTimetableEventParams params, {
|
||||
super.onUpdate,
|
||||
super.onError,
|
||||
super.renew,
|
||||
}) : super(
|
||||
RequestCache.cacheMinute,
|
||||
onUpdate,
|
||||
onError: onError ?? RequestCache.ignore,
|
||||
renew: renew,
|
||||
cacheTime: RequestCache.cacheMinute,
|
||||
loader: () => GetCustomTimetableEvent(params).run(),
|
||||
fromJson: GetCustomTimetableEventResponse.fromJson,
|
||||
) {
|
||||
start('customTimetableEvents');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetCustomTimetableEventResponse> onLoad() => GetCustomTimetableEvent(params).run();
|
||||
|
||||
@override
|
||||
GetCustomTimetableEventResponse onLocalData(String json) => GetCustomTimetableEventResponse.fromJson(jsonDecode(json));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../../../../../model/accountData.dart';
|
||||
import '../../../../../model/account_data.dart';
|
||||
import '../../../mhslApi.dart';
|
||||
import 'updateUserIndexParams.dart';
|
||||
|
||||
|
||||
@@ -15,6 +15,15 @@ abstract class RequestCache<T extends ApiResponse?> {
|
||||
|
||||
int maxCacheTime;
|
||||
void Function(T)? onUpdate;
|
||||
|
||||
/// Called only when [start] finds a cached payload in localstore. Use this
|
||||
/// (instead of [onUpdate]) when callers need to distinguish stale-but-fast
|
||||
/// cache hits from authoritative network responses.
|
||||
void Function(T)? onCacheData;
|
||||
|
||||
/// Called only when [start] receives a fresh payload from the network.
|
||||
void Function(T)? onNetworkData;
|
||||
|
||||
void Function(Exception) onError;
|
||||
bool? renew;
|
||||
|
||||
@@ -26,7 +35,14 @@ abstract class RequestCache<T extends ApiResponse?> {
|
||||
/// attempt have settled.
|
||||
Future<void> get ready => _ready.future;
|
||||
|
||||
RequestCache(this.maxCacheTime, this.onUpdate, {this.onError = ignore, this.renew = false});
|
||||
RequestCache(
|
||||
this.maxCacheTime,
|
||||
this.onUpdate, {
|
||||
this.onError = ignore,
|
||||
this.renew = false,
|
||||
this.onCacheData,
|
||||
this.onNetworkData,
|
||||
});
|
||||
|
||||
static void ignore(Exception e) {}
|
||||
|
||||
@@ -34,7 +50,9 @@ abstract class RequestCache<T extends ApiResponse?> {
|
||||
try {
|
||||
final tableData = await Localstore.instance.collection(collection).doc(document).get();
|
||||
if (tableData != null) {
|
||||
onUpdate?.call(onLocalData(tableData['json']));
|
||||
final cached = onLocalData(tableData['json']);
|
||||
onUpdate?.call(cached);
|
||||
onCacheData?.call(cached);
|
||||
}
|
||||
|
||||
if (DateTime.now().millisecondsSinceEpoch - (maxCacheTime * 1000) < (tableData?['lastupdate'] ?? 0)) {
|
||||
@@ -44,6 +62,7 @@ abstract class RequestCache<T extends ApiResponse?> {
|
||||
try {
|
||||
final newValue = await onLoad();
|
||||
onUpdate?.call(newValue);
|
||||
onNetworkData?.call(newValue);
|
||||
Localstore.instance.collection(collection).doc(document).set({
|
||||
'json': jsonEncode(newValue),
|
||||
'lastupdate': DateTime.now().millisecondsSinceEpoch,
|
||||
@@ -60,3 +79,38 @@ abstract class RequestCache<T extends ApiResponse?> {
|
||||
Future<T> onLoad();
|
||||
|
||||
}
|
||||
|
||||
/// Concrete [RequestCache] that delegates the two overrides to functions
|
||||
/// passed in the constructor. Used to collapse the dozens of one-class-per-
|
||||
/// endpoint cache files that all just forward to `<Endpoint>().run()` and
|
||||
/// `<Response>.fromJson(jsonDecode(...))`.
|
||||
class SimpleCache<T extends ApiResponse?> extends RequestCache<T> {
|
||||
final Future<T> Function() _loader;
|
||||
final T Function(Map<String, dynamic> json) _fromJson;
|
||||
|
||||
SimpleCache({
|
||||
required int cacheTime,
|
||||
required Future<T> Function() loader,
|
||||
required T Function(Map<String, dynamic> json) fromJson,
|
||||
void Function(T)? onUpdate,
|
||||
void Function(T)? onCacheData,
|
||||
void Function(T)? onNetworkData,
|
||||
void Function(Exception)? onError,
|
||||
bool? renew,
|
||||
}) : _loader = loader,
|
||||
_fromJson = fromJson,
|
||||
super(
|
||||
cacheTime,
|
||||
onUpdate,
|
||||
onError: onError ?? RequestCache.ignore,
|
||||
renew: renew,
|
||||
onCacheData: onCacheData,
|
||||
onNetworkData: onNetworkData,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<T> onLoad() => _loader();
|
||||
|
||||
@override
|
||||
T onLocalData(String json) => _fromJson(jsonDecode(json));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../../model/accountData.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../webuntisApi.dart';
|
||||
import 'authenticateParams.dart';
|
||||
import 'authenticateResponse.dart';
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getHolidays.dart';
|
||||
import 'getHolidaysResponse.dart';
|
||||
|
||||
class GetHolidaysCache extends RequestCache<GetHolidaysResponse> {
|
||||
GetHolidaysCache({
|
||||
void Function(GetHolidaysResponse)? onUpdate,
|
||||
void Function(Exception)? onError,
|
||||
bool? renew,
|
||||
}) : super(
|
||||
RequestCache.cacheDay,
|
||||
onUpdate,
|
||||
onError: onError ?? RequestCache.ignore,
|
||||
renew: renew,
|
||||
class GetHolidaysCache extends SimpleCache<GetHolidaysResponse> {
|
||||
GetHolidaysCache({super.onUpdate, super.onError, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheDay,
|
||||
loader: () => GetHolidays().run(),
|
||||
fromJson: GetHolidaysResponse.fromJson,
|
||||
) {
|
||||
start('wu-holidays');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetHolidaysResponse> onLoad() => GetHolidays().run();
|
||||
|
||||
@override
|
||||
GetHolidaysResponse onLocalData(String json) => GetHolidaysResponse.fromJson(jsonDecode(json));
|
||||
}
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getRooms.dart';
|
||||
import 'getRoomsResponse.dart';
|
||||
|
||||
class GetRoomsCache extends RequestCache<GetRoomsResponse> {
|
||||
GetRoomsCache({
|
||||
void Function(GetRoomsResponse)? onUpdate,
|
||||
void Function(Exception)? onError,
|
||||
bool? renew,
|
||||
}) : super(
|
||||
RequestCache.cacheHour,
|
||||
onUpdate,
|
||||
onError: onError ?? RequestCache.ignore,
|
||||
renew: renew,
|
||||
class GetRoomsCache extends SimpleCache<GetRoomsResponse> {
|
||||
GetRoomsCache({super.onUpdate, super.onError, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheHour,
|
||||
loader: () => GetRooms().run(),
|
||||
fromJson: GetRoomsResponse.fromJson,
|
||||
) {
|
||||
start('wu-rooms');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetRoomsResponse> onLoad() => GetRooms().run();
|
||||
|
||||
@override
|
||||
GetRoomsResponse onLocalData(String json) => GetRoomsResponse.fromJson(jsonDecode(json));
|
||||
|
||||
}
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getSubjects.dart';
|
||||
import 'getSubjectsResponse.dart';
|
||||
|
||||
class GetSubjectsCache extends RequestCache<GetSubjectsResponse> {
|
||||
GetSubjectsCache({
|
||||
void Function(GetSubjectsResponse)? onUpdate,
|
||||
void Function(Exception)? onError,
|
||||
bool? renew,
|
||||
}) : super(
|
||||
RequestCache.cacheHour,
|
||||
onUpdate,
|
||||
onError: onError ?? RequestCache.ignore,
|
||||
renew: renew,
|
||||
class GetSubjectsCache extends SimpleCache<GetSubjectsResponse> {
|
||||
GetSubjectsCache({super.onUpdate, super.onError, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheHour,
|
||||
loader: () => GetSubjects().run(),
|
||||
fromJson: GetSubjectsResponse.fromJson,
|
||||
) {
|
||||
start('wu-subjects');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetSubjectsResponse> onLoad() => GetSubjects().run();
|
||||
|
||||
@override
|
||||
onLocalData(String json) => GetSubjectsResponse.fromJson(jsonDecode(json));
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import 'getTimegridUnits.dart';
|
||||
import 'getTimegridUnitsResponse.dart';
|
||||
|
||||
class GetTimegridUnitsCache extends RequestCache<GetTimegridUnitsResponse> {
|
||||
GetTimegridUnitsCache({
|
||||
void Function(GetTimegridUnitsResponse)? onUpdate,
|
||||
bool? renew,
|
||||
}) : super(RequestCache.cacheDay, onUpdate, renew: renew) {
|
||||
class GetTimegridUnitsCache extends SimpleCache<GetTimegridUnitsResponse> {
|
||||
GetTimegridUnitsCache({super.onUpdate, super.renew})
|
||||
: super(
|
||||
cacheTime: RequestCache.cacheDay,
|
||||
loader: () => GetTimegridUnits().run(),
|
||||
fromJson: GetTimegridUnitsResponse.fromJson,
|
||||
) {
|
||||
start('wu-timegrid');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GetTimegridUnitsResponse> onLoad() => GetTimegridUnits().run();
|
||||
|
||||
@override
|
||||
GetTimegridUnitsResponse onLocalData(String json) => GetTimegridUnitsResponse.fromJson(jsonDecode(json));
|
||||
}
|
||||
|
||||
@@ -1,40 +1,33 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../requestCache.dart';
|
||||
import '../authenticate/authenticate.dart';
|
||||
import 'getTimetable.dart';
|
||||
import 'getTimetableParams.dart';
|
||||
import 'getTimetableResponse.dart';
|
||||
|
||||
class GetTimetableCache extends RequestCache<GetTimetableResponse> {
|
||||
int startdate;
|
||||
int enddate;
|
||||
|
||||
class GetTimetableCache extends SimpleCache<GetTimetableResponse> {
|
||||
GetTimetableCache({
|
||||
required void Function(GetTimetableResponse) onUpdate,
|
||||
void Function(Exception)? onError,
|
||||
required this.startdate,
|
||||
required this.enddate,
|
||||
bool? renew,
|
||||
super.onError,
|
||||
required int startdate,
|
||||
required int enddate,
|
||||
super.renew,
|
||||
}) : super(
|
||||
RequestCache.cacheMinute,
|
||||
onUpdate,
|
||||
onError: onError ?? RequestCache.ignore,
|
||||
renew: renew,
|
||||
cacheTime: RequestCache.cacheMinute,
|
||||
loader: () => _load(startdate, enddate),
|
||||
fromJson: GetTimetableResponse.fromJson,
|
||||
onUpdate: onUpdate,
|
||||
) {
|
||||
start('wu-timetable-$startdate-$enddate');
|
||||
}
|
||||
|
||||
@override
|
||||
GetTimetableResponse onLocalData(String json) => GetTimetableResponse.fromJson(jsonDecode(json));
|
||||
|
||||
@override
|
||||
Future<GetTimetableResponse> onLoad() async => GetTimetable(
|
||||
static Future<GetTimetableResponse> _load(int startdate, int enddate) async {
|
||||
final session = await Authenticate.getSession();
|
||||
return GetTimetable(
|
||||
GetTimetableParams(
|
||||
options: GetTimetableParamsOptions(
|
||||
element: GetTimetableParamsOptionsElement(
|
||||
id: (await Authenticate.getSession()).personId,
|
||||
type: (await Authenticate.getSession()).personType,
|
||||
id: session.personId,
|
||||
type: session.personType,
|
||||
keyType: GetTimetableParamsOptionsElementKeyType.id,
|
||||
),
|
||||
startDate: startdate,
|
||||
@@ -43,7 +36,8 @@ class GetTimetableCache extends RequestCache<GetTimetableResponse> {
|
||||
subjectFields: GetTimetableParamsOptionsFields.all,
|
||||
roomFields: GetTimetableParamsOptionsFields.all,
|
||||
klasseFields: GetTimetableParamsOptionsFields.all,
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
).run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../model/endpointData.dart';
|
||||
import '../../model/endpoint_data.dart';
|
||||
import '../apiParams.dart';
|
||||
import '../apiRequest.dart';
|
||||
import '../apiResponse.dart';
|
||||
|
||||
+5
-5
@@ -11,10 +11,10 @@ import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
|
||||
import 'api/mhsl/server/userIndex/update/updateUserindex.dart';
|
||||
import 'main.dart';
|
||||
import 'widget/breaker/breaker.dart';
|
||||
import 'model/dataCleaner.dart';
|
||||
import 'notification/notificationController.dart';
|
||||
import 'notification/notificationTasks.dart';
|
||||
import 'notification/notifyUpdater.dart';
|
||||
import 'model/data_cleaner.dart';
|
||||
import 'notification/notification_controller.dart';
|
||||
import 'notification/notification_tasks.dart';
|
||||
import 'notification/notify_updater.dart';
|
||||
import 'state/app/modules/app_modules.dart';
|
||||
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
|
||||
import 'state/app/modules/chatList/bloc/chat_list_bloc.dart';
|
||||
@@ -106,7 +106,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
controller: Main.bottomNavigator,
|
||||
navBarOverlap: const NavBarOverlap.none(),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
handleAndroidBackButtonPress: false,
|
||||
handleAndroidBackButtonPress: true,
|
||||
screenTransitionAnimation: const ScreenTransitionAnimation(
|
||||
curve: Curves.easeOutQuad,
|
||||
duration: Duration(milliseconds: 200),
|
||||
|
||||
+16
-7
@@ -19,7 +19,7 @@ import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
|
||||
import 'app.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'model/accountData.dart';
|
||||
import 'model/account_data.dart';
|
||||
import 'widget/breaker/breaker.dart';
|
||||
import 'state/app/modules/account/bloc/account_bloc.dart';
|
||||
import 'state/app/modules/account/bloc/account_state.dart';
|
||||
@@ -28,11 +28,11 @@ import 'state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import 'state/app/modules/chatList/bloc/chat_list_bloc.dart';
|
||||
import 'state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import 'storage/base/settings.dart';
|
||||
import 'theming/darkAppTheme.dart';
|
||||
import 'theming/lightAppTheme.dart';
|
||||
import 'storage/settings.dart';
|
||||
import 'theming/dark_app_theme.dart';
|
||||
import 'theming/light_app_theme.dart';
|
||||
import 'view/login/login.dart';
|
||||
import 'widget/placeholderView.dart';
|
||||
import 'widget/placeholder_view.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
log('MarianumMobile started');
|
||||
@@ -43,7 +43,7 @@ Future<void> main() async {
|
||||
|
||||
final initialisationTasks = [
|
||||
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform)
|
||||
.then((_) async => log('Firebase token: ${await FirebaseMessaging.instance.getToken() ?? "Error: no Firebase token!"}'))
|
||||
.then<void>((_) {})
|
||||
.onError((error, _) => log('Error initializing Firebase: $error')),
|
||||
PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem').then(addCertificateAsTrusted),
|
||||
PlatformAssetBundle().load('assets/ca/lets-encrypt-r10.pem').then(addCertificateAsTrusted),
|
||||
@@ -54,12 +54,19 @@ Future<void> main() async {
|
||||
);
|
||||
HydratedBloc.storage = storage;
|
||||
}),
|
||||
AccountData().waitForPopulation(),
|
||||
];
|
||||
|
||||
log('starting app initialisation...');
|
||||
await Future.wait(initialisationTasks);
|
||||
log('app initialisation done!');
|
||||
|
||||
unawaited(
|
||||
FirebaseMessaging.instance.getToken().then(
|
||||
(token) => log('Firebase token: ${token ?? "Error: no Firebase token!"}'),
|
||||
),
|
||||
);
|
||||
|
||||
if (kReleaseMode) {
|
||||
ErrorWidget.builder = (error) => PlaceholderView(
|
||||
icon: Icons.phonelink_erase_rounded,
|
||||
@@ -83,7 +90,9 @@ Future<void> main() async {
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
|
||||
BlocProvider<AccountBloc>(create: (_) => AccountBloc()),
|
||||
BlocProvider<AccountBloc>(create: (_) => AccountBloc(
|
||||
initialStatus: AccountData().isPopulated() ? AccountStatus.loggedIn : AccountStatus.loggedOut,
|
||||
)),
|
||||
BlocProvider<BreakerBloc>(create: (_) => BreakerBloc()),
|
||||
BlocProvider<ChatListBloc>(create: (_) => ChatListBloc()),
|
||||
BlocProvider<ChatBloc>(create: (_) => ChatBloc()),
|
||||
|
||||
@@ -98,8 +98,15 @@ class AccountData {
|
||||
|
||||
bool isPopulated() => _username != null && _password != null;
|
||||
|
||||
String buildHttpAuthString() {
|
||||
/// Returns the value for an HTTP `Authorization` header using HTTP Basic.
|
||||
/// Prefer this over embedding credentials in URLs — error logs and crash
|
||||
/// reports often capture the URL but not headers.
|
||||
String getBasicAuthHeader() {
|
||||
if (!isPopulated()) throw Exception('AccountData (e.g. username or password) is not initialized!');
|
||||
return '$_username:$_password';
|
||||
return 'Basic ${base64Encode(utf8.encode('$_username:$_password'))}';
|
||||
}
|
||||
|
||||
/// Convenience wrapper around [getBasicAuthHeader] returning a single-entry
|
||||
/// header map ready to merge into HTTP request headers.
|
||||
Map<String, String> authHeaders() => {'Authorization': getBasicAuthHeader()};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import 'accountData.dart';
|
||||
import 'account_data.dart';
|
||||
|
||||
enum EndpointMode {
|
||||
live,
|
||||
+12
-4
@@ -1,9 +1,9 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widget/debug/debugTile.dart';
|
||||
import '../widget/debug/jsonViewer.dart';
|
||||
import 'notificationTasks.dart';
|
||||
import '../widget/debug/debug_tile.dart';
|
||||
import '../widget/debug/json_viewer.dart';
|
||||
import 'notification_tasks.dart';
|
||||
|
||||
class NotificationController {
|
||||
@pragma('vm:entry-point')
|
||||
@@ -44,7 +44,7 @@ class NotificationController {
|
||||
}
|
||||
|
||||
static Future<void> onAppOpenedByNotification(RemoteMessage message, BuildContext context) async {
|
||||
NotificationTasks.navigateToTalk(context);
|
||||
NotificationTasks.navigateToTalk(context, chatToken: _extractChatToken(message));
|
||||
NotificationTasks.updateProviders(context);
|
||||
|
||||
DebugTile(context).run(() {
|
||||
@@ -60,4 +60,12 @@ class NotificationController {
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
static String? _extractChatToken(RemoteMessage message) {
|
||||
for (final key in const ['chatToken', 'token', 'roomToken']) {
|
||||
final value = message.data[key];
|
||||
if (value is String && value.isNotEmpty) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../state/app/modules/app_modules.dart';
|
||||
import '../routing/app_routes.dart';
|
||||
import '../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../state/app/modules/chatList/bloc/chat_list_bloc.dart';
|
||||
|
||||
@@ -18,9 +17,14 @@ class NotificationTasks {
|
||||
context.read<ChatBloc>().refresh();
|
||||
}
|
||||
|
||||
static void navigateToTalk(BuildContext context) {
|
||||
final talkTab = AppModule.getBottomBarModules(context).map((e) => e.module).toList().indexOf(Modules.talk);
|
||||
if (talkTab == -1) return;
|
||||
Main.bottomNavigator.jumpToTab(talkTab);
|
||||
/// Switches to the Talk tab. If [chatToken] is provided, also schedules
|
||||
/// the matching chat to be opened automatically once the chat list view
|
||||
/// resolves the token (handled inside [ChatList]).
|
||||
static void navigateToTalk(BuildContext context, {String? chatToken}) {
|
||||
if (chatToken != null && chatToken.isNotEmpty) {
|
||||
AppRoutes.openChatByToken(context, chatToken);
|
||||
} else {
|
||||
AppRoutes.goToTalkTab(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/mhsl/notify/register/notifyRegister.dart';
|
||||
import '../api/mhsl/notify/register/notifyRegisterParams.dart';
|
||||
import '../model/accountData.dart';
|
||||
import '../model/account_data.dart';
|
||||
import '../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../widget/confirmDialog.dart';
|
||||
import '../widget/confirm_dialog.dart';
|
||||
|
||||
class NotifyUpdater {
|
||||
static ConfirmDialog enableAfterDisclaimer(SettingsCubit settings) => ConfirmDialog(
|
||||
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
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/talk/room/getRoomResponse.dart';
|
||||
import '../main.dart';
|
||||
import '../model/account_data.dart';
|
||||
import '../state/app/modules/app_modules.dart';
|
||||
import '../state/app/modules/chatList/bloc/chat_list_bloc.dart';
|
||||
import '../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
|
||||
import '../view/pages/files/files.dart';
|
||||
import '../view/pages/marianum_message/marianum_message_view.dart';
|
||||
import '../view/pages/more/feedback/feedback_dialog.dart';
|
||||
import '../view/pages/more/roomplan/roomplan.dart';
|
||||
import '../view/pages/more/share/qr_share_view.dart';
|
||||
import '../view/pages/talk/chat_view.dart';
|
||||
import '../view/pages/talk/details/message_reactions.dart';
|
||||
import '../view/pages/talk/talk_navigator.dart';
|
||||
import '../view/pages/timetable/custom_events/custom_events_view.dart';
|
||||
import '../view/pages/settings/settings.dart';
|
||||
import '../widget/debug/cache_view.dart';
|
||||
import '../widget/file_viewer.dart';
|
||||
import '../widget/user_avatar.dart';
|
||||
|
||||
/// Single entry point for full-page navigations across the app.
|
||||
///
|
||||
/// Every full-page push in modules should go through one of these methods.
|
||||
/// Dialogs (`showDialog`), bottom sheets (`showStickyFlexibleBottomSheet`,
|
||||
/// `showAppointmentBottomSheet`), and `Navigator.pop` for closing those
|
||||
/// remain unchanged and live at the call sites.
|
||||
class AppRoutes {
|
||||
AppRoutes._();
|
||||
|
||||
/// Token of a chat that should be auto-opened in the Talk tab once
|
||||
/// the chat list view picks it up. Set by [openChatByToken] (e.g. from
|
||||
/// a tapped notification) and consumed by the `ChatList` widget.
|
||||
static final ValueNotifier<String?> pendingChatToken = ValueNotifier(null);
|
||||
|
||||
// -- Files --------------------------------------------------------------
|
||||
|
||||
static void openFolder(BuildContext context, List<String> path) {
|
||||
pushScreen(context, withNavBar: false, screen: Files(path: path));
|
||||
}
|
||||
|
||||
static void openFileViewer(BuildContext context, String localPath, {bool openExternal = false}) {
|
||||
pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FileViewer(path: localPath, openExternal: openExternal),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Timetable ----------------------------------------------------------
|
||||
|
||||
static void openCustomEvents(BuildContext context) {
|
||||
pushScreen(context, withNavBar: false, screen: const CustomEventsView());
|
||||
}
|
||||
|
||||
// -- Marianum Message ---------------------------------------------------
|
||||
|
||||
static void openMarianumMessage(BuildContext context, String basePath, MarianumMessage message) {
|
||||
pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: MessageView(basePath: basePath, message: message),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Sharing / Settings / Feedback / DevTools ---------------------------
|
||||
|
||||
static void openQrShare(BuildContext context) {
|
||||
pushScreen(context, withNavBar: false, screen: const QrShareView());
|
||||
}
|
||||
|
||||
static void openSettings(BuildContext context) {
|
||||
pushScreen(context, withNavBar: false, screen: const Settings());
|
||||
}
|
||||
|
||||
static void openFeedback(BuildContext context) {
|
||||
pushScreen(context, withNavBar: false, screen: const FeedbackDialog());
|
||||
}
|
||||
|
||||
static void openCacheView(BuildContext context) {
|
||||
pushScreen(context, withNavBar: false, screen: const CacheView());
|
||||
}
|
||||
|
||||
static void openRoomplan(BuildContext context) {
|
||||
pushScreen(context, withNavBar: false, screen: const Roomplan());
|
||||
}
|
||||
|
||||
// -- Talk ---------------------------------------------------------------
|
||||
|
||||
static void openMessageReactions(BuildContext context, String token, int messageId) {
|
||||
pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: MessageReactions(token: token, messageId: messageId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Opens a chat from a known [GetRoomResponseObject]. Delegates to
|
||||
/// [TalkNavigator.pushSplitView] so tablet split-view behaviour stays intact.
|
||||
static void openChatView(
|
||||
BuildContext context, {
|
||||
required GetRoomResponseObject room,
|
||||
required String selfId,
|
||||
required UserAvatar avatar,
|
||||
bool overrideToSingleSubScreen = true,
|
||||
}) {
|
||||
TalkNavigator.pushSplitView(
|
||||
context,
|
||||
ChatView(room: room, selfId: selfId, avatar: avatar),
|
||||
overrideToSingleSubScreen: overrideToSingleSubScreen,
|
||||
);
|
||||
context.read<ChatBloc>().setToken(room.token);
|
||||
}
|
||||
|
||||
/// Schedules a chat to be opened in the Talk tab. Use this when only the
|
||||
/// token is known (e.g. from a tapped notification) — the actual push
|
||||
/// happens inside the `ChatList` widget once the room is available.
|
||||
static void openChatByToken(BuildContext context, String token) {
|
||||
pendingChatToken.value = token;
|
||||
goToTalkTab(context);
|
||||
try {
|
||||
context.read<ChatListBloc>().refresh();
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('openChatByToken: ChatListBloc refresh failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a pending chat token (set via [openChatByToken]) using the
|
||||
/// [ChatListBloc]'s current rooms and the active [AccountData] credentials.
|
||||
/// Returns `null` if the token cannot yet be matched (e.g. the room is
|
||||
/// still being loaded). Callers should keep listening to [pendingChatToken]
|
||||
/// and the bloc state and retry when either changes.
|
||||
static ResolvedPendingChat? resolvePendingChat(BuildContext context) {
|
||||
final token = pendingChatToken.value;
|
||||
if (token == null) return null;
|
||||
if (!AccountData().isPopulated()) return null;
|
||||
|
||||
final rooms = context.read<ChatListBloc>().state.data?.rooms;
|
||||
final room = _findRoomByToken(rooms, token);
|
||||
if (room == null) return null;
|
||||
|
||||
final isGroup = room.type != GetRoomResponseObjectConversationType.oneToOne;
|
||||
final avatar = UserAvatar(id: isGroup ? room.token : room.name, isGroup: isGroup);
|
||||
return ResolvedPendingChat(
|
||||
room: room,
|
||||
selfId: AccountData().getUsername(),
|
||||
avatar: avatar,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// -- Module / Tab navigation -------------------------------------------
|
||||
|
||||
/// Opens an [AppModule]'s root view as a full screen push (used by the
|
||||
/// "Mehr" tab list). Modules that live in the bottom bar are reached via
|
||||
/// [goToTab] instead.
|
||||
static void openModule(BuildContext context, AppModule module) {
|
||||
pushScreen(context, withNavBar: false, screen: module.create());
|
||||
}
|
||||
|
||||
/// Switches the bottom navigation to the given [module] if it is currently
|
||||
/// in the bottom bar. Returns `true` if the jump happened.
|
||||
static bool goToTab(BuildContext context, Modules module) {
|
||||
final index = AppModule.getBottomBarModules(context)
|
||||
.map((e) => e.module)
|
||||
.toList()
|
||||
.indexOf(module);
|
||||
if (index == -1) return false;
|
||||
Main.bottomNavigator.jumpToTab(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Convenience wrapper for the Talk tab — preserved for the notification
|
||||
/// handler API which only knows about Talk.
|
||||
static void goToTalkTab(BuildContext context) {
|
||||
goToTab(context, Modules.talk);
|
||||
}
|
||||
}
|
||||
|
||||
class ResolvedPendingChat {
|
||||
final GetRoomResponseObject room;
|
||||
final String selfId;
|
||||
final UserAvatar avatar;
|
||||
|
||||
const ResolvedPendingChat({
|
||||
required this.room,
|
||||
required this.selfId,
|
||||
required this.avatar,
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,21 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
final Widget Function(TState state, bool loading) child;
|
||||
final void Function(TState state)? onLoad;
|
||||
final bool wrapWithScrollView;
|
||||
const LoadableStateConsumer({required this.child, this.onLoad, this.wrapWithScrollView = false, super.key});
|
||||
|
||||
/// Optional predicate for callers whose [TState] always contains a non-null
|
||||
/// envelope but where actual content (e.g. a nested response) is loaded
|
||||
/// lazily. When provided, this overrides the default `data != null` check
|
||||
/// so primary loading / error screens / content visibility correctly reflect
|
||||
/// whether the inner content is ready.
|
||||
final bool Function(TState state)? isReady;
|
||||
|
||||
const LoadableStateConsumer({
|
||||
required this.child,
|
||||
this.onLoad,
|
||||
this.wrapWithScrollView = false,
|
||||
this.isReady,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static Duration animationDuration = const Duration(milliseconds: 200);
|
||||
|
||||
@@ -30,6 +44,16 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => onLoad!(loadedData));
|
||||
}
|
||||
|
||||
final typedData = loadedData is TState ? loadedData : null;
|
||||
final hasContent = typedData != null && (isReady?.call(typedData) ?? loadableState.showContent());
|
||||
final hasError = loadableState.error != null;
|
||||
final isLoading = loadableState.isLoading;
|
||||
|
||||
final showPrimaryLoading = isLoading && !hasContent;
|
||||
final showBackgroundLoading = isLoading && hasContent;
|
||||
final showError = hasError && !hasContent;
|
||||
final showErrorBar = hasError && hasContent;
|
||||
|
||||
var childWidget = ConditionalWrapper(
|
||||
condition: loadableState.reFetch != null,
|
||||
wrapper: (child) => RefreshIndicator(
|
||||
@@ -48,8 +72,8 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: loadableState.showContent() && loadedData is TState
|
||||
? child(loadedData, loadableState.isLoading)
|
||||
child: hasContent
|
||||
? child(typedData as TState, isLoading)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
@@ -60,16 +84,21 @@ class LoadableStateConsumer<TController extends Bloc<LoadableHydratedBlocEvent<T
|
||||
bloc.reFetch = loadableState.reFetch;
|
||||
return Column(
|
||||
children: [
|
||||
LoadableStateErrorBar(visible: loadableState.showErrorBar(), message: loadableState.error?.message, lastUpdated: loadableState.lastFetch),
|
||||
LoadableStateErrorBar(
|
||||
visible: showErrorBar,
|
||||
hasContent: hasContent,
|
||||
message: loadableState.error?.message,
|
||||
lastUpdated: loadableState.lastFetch,
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
LoadableStatePrimaryLoading(visible: loadableState.showPrimaryLoading()),
|
||||
LoadableStateBackgroundLoading(visible: loadableState.showBackgroundLoading()),
|
||||
LoadableStateErrorScreen(visible: loadableState.showError(), message: loadableState.error?.message),
|
||||
LoadableStatePrimaryLoading(visible: showPrimaryLoading),
|
||||
LoadableStateBackgroundLoading(visible: showBackgroundLoading),
|
||||
LoadableStateErrorScreen(visible: showError, message: loadableState.error?.message),
|
||||
|
||||
AnimatedOpacity(
|
||||
opacity: loadableState.showContent() ? 1.0 : 0.0,
|
||||
opacity: hasContent ? 1.0 : 0.0,
|
||||
duration: animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: childWidget,
|
||||
|
||||
@@ -3,19 +3,31 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../widget/infoDialog.dart';
|
||||
import '../../../../../widget/info_dialog.dart';
|
||||
import '../bloc/loadable_state_bloc.dart';
|
||||
|
||||
class LoadableStateErrorBar extends StatelessWidget {
|
||||
final bool visible;
|
||||
final bool hasContent;
|
||||
final String? message;
|
||||
final int? lastUpdated;
|
||||
const LoadableStateErrorBar({required this.visible, this.message, this.lastUpdated, super.key});
|
||||
const LoadableStateErrorBar({
|
||||
required this.visible,
|
||||
this.hasContent = false,
|
||||
this.message,
|
||||
this.lastUpdated,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Duration animationDuration = const Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedSize(
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.watch<LoadableStateBloc>();
|
||||
final isOfflineWithCache = hasContent && bloc.connectivityStatusKnown() && !bloc.isConnected();
|
||||
final shouldShow = visible || isOfflineWithCache;
|
||||
|
||||
return AnimatedSize(
|
||||
duration: animationDuration,
|
||||
child: AnimatedSwitcher(
|
||||
duration: animationDuration,
|
||||
@@ -27,8 +39,8 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
child: child,
|
||||
),
|
||||
child: Visibility(
|
||||
key: Key(visible.hashCode.toString()),
|
||||
visible: visible,
|
||||
key: Key(shouldShow.hashCode.toString()),
|
||||
visible: shouldShow,
|
||||
replacement: const SizedBox(width: double.infinity),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
@@ -51,6 +63,7 @@ class LoadableStateErrorBar extends StatelessWidget {
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoadableStateErrorBarText extends StatefulWidget {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'account_event.dart';
|
||||
import 'account_state.dart';
|
||||
|
||||
class AccountBloc extends Bloc<AccountEvent, AccountState> {
|
||||
AccountBloc() : super(const AccountState()) {
|
||||
AccountBloc({AccountStatus initialStatus = AccountStatus.undefined}) : super(AccountState(status: initialStatus)) {
|
||||
on<AccountStatusChanged>((event, emit) => emit(state.copyWith(status: event.status)));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,24 @@ import 'package:flutter/material.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
|
||||
import '../../../api/mhsl/breaker/getBreakers/getBreakersResponse.dart';
|
||||
import '../../../widget/breaker/breaker.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../view/pages/files/files.dart';
|
||||
import '../../../view/pages/grade_averages/grade_averages_view.dart';
|
||||
import '../../../view/pages/holidays/holidays_view.dart';
|
||||
import '../../../view/pages/marianum_dates/marianum_dates_view.dart';
|
||||
import '../../../view/pages/marianum_message/marianum_message_list_view.dart';
|
||||
import '../../../view/pages/more/roomplan/roomplan.dart';
|
||||
import '../../../view/pages/talk/chatList.dart';
|
||||
import '../../../view/pages/talk/chat_list.dart';
|
||||
import '../../../view/pages/timetable/timetable.dart';
|
||||
import '../../../widget/centeredLeading.dart';
|
||||
import '../../../widget/breaker/breaker.dart';
|
||||
import '../../../widget/centered_leading.dart';
|
||||
import '../infrastructure/loadableState/loadable_state.dart';
|
||||
import 'chatList/bloc/chat_list_bloc.dart';
|
||||
import 'chatList/bloc/chat_list_state.dart';
|
||||
import 'settings/bloc/settings_cubit.dart';
|
||||
import '../infrastructure/loadableState/loadable_state.dart';
|
||||
import 'gradeAverages/view/grade_averages_view.dart';
|
||||
import 'holidays/view/holidays_view.dart';
|
||||
import 'marianumMessage/view/marianum_message_list_view.dart';
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
|
||||
class AppModule {
|
||||
Modules module;
|
||||
@@ -98,6 +100,13 @@ class AppModule {
|
||||
breakerArea: BreakerArea.more,
|
||||
create: HolidaysView.new,
|
||||
),
|
||||
Modules.marianumDates: AppModule(
|
||||
Modules.marianumDates,
|
||||
name: 'Marianum Termine',
|
||||
icon: () => Icon(Icons.event_note),
|
||||
breakerArea: BreakerArea.more,
|
||||
create: MarianumDatesView.new,
|
||||
),
|
||||
};
|
||||
|
||||
if(!showFiltered) available.removeWhere((key, value) => settings.val().modulesSettings.hiddenModules.contains(key));
|
||||
@@ -112,7 +121,7 @@ class AppModule {
|
||||
key: key,
|
||||
leading: CenteredLeading(icon()),
|
||||
title: Text(name),
|
||||
onTap: isReorder ? null : () => pushScreen(context, withNavBar: false, screen: create()),
|
||||
onTap: isReorder ? null : () => AppRoutes.openModule(context, this),
|
||||
trailing: isReorder
|
||||
? Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(onPressed: onVisibleChange, icon: Icon(isVisible ? Icons.visibility_outlined : Icons.visibility_off_outlined)),
|
||||
@@ -140,4 +149,5 @@ enum Modules {
|
||||
roomPlan,
|
||||
gradeAveragesCalculator,
|
||||
holidays,
|
||||
marianumDates,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '../../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../infrastructure/loadableState/loading_error.dart';
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../repository/chat_repository.dart';
|
||||
@@ -22,8 +24,11 @@ class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final token = innerState?.currentToken ?? '';
|
||||
if (token.isEmpty) return;
|
||||
_loadChat(token);
|
||||
if (token.isEmpty) {
|
||||
add(DataGathered((s) => s));
|
||||
return;
|
||||
}
|
||||
await _loadChat(token);
|
||||
}
|
||||
|
||||
void setToken(String token) {
|
||||
@@ -32,6 +37,7 @@ class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository
|
||||
return;
|
||||
}
|
||||
add(Emit((s) => s.copyWith(currentToken: token, chatResponse: null)));
|
||||
add(RefetchStarted<ChatState>());
|
||||
_loadChat(token);
|
||||
}
|
||||
|
||||
@@ -41,19 +47,37 @@ class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository
|
||||
|
||||
void refresh() {
|
||||
final token = innerState?.currentToken ?? '';
|
||||
if (token.isNotEmpty) _loadChat(token);
|
||||
if (token.isEmpty) return;
|
||||
add(RefetchStarted<ChatState>());
|
||||
_loadChat(token);
|
||||
}
|
||||
|
||||
void _loadChat(String token) {
|
||||
Future<void> _loadChat(String token) async {
|
||||
final requestStart = DateTime.now();
|
||||
_lastTokenSet = requestStart;
|
||||
repo.data.getChat(
|
||||
|
||||
Object? capturedError;
|
||||
GetChatResponse? response;
|
||||
try {
|
||||
response = await repo.data.getChat(
|
||||
token: token,
|
||||
onUpdate: (data) {
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
} catch (e) {
|
||||
capturedError = e;
|
||||
}
|
||||
|
||||
if (_lastTokenSet.isAfter(requestStart)) return;
|
||||
if ((innerState?.currentToken ?? '') != token) return;
|
||||
add(DataGathered((s) => s.copyWith(chatResponse: data)));
|
||||
},
|
||||
);
|
||||
|
||||
if (response != null) {
|
||||
add(DataGathered((s) => s.copyWith(chatResponse: response)));
|
||||
}
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: capturedError.toString(),
|
||||
allowRetry: true,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,26 @@ import '../../../../../api/marianumcloud/talk/chat/getChatCache.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
|
||||
class ChatDataProvider {
|
||||
void getChat({
|
||||
Future<GetChatResponse> getChat({
|
||||
required String token,
|
||||
required void Function(GetChatResponse data) onUpdate,
|
||||
}) {
|
||||
GetChatCache(chatToken: token, onUpdate: onUpdate);
|
||||
void Function(GetChatResponse data)? onUpdate,
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
GetChatResponse? latest;
|
||||
Object? capturedError;
|
||||
final cache = GetChatCache(
|
||||
chatToken: token,
|
||||
onUpdate: (data) {
|
||||
latest = data;
|
||||
onUpdate?.call(data);
|
||||
},
|
||||
onError: (e) {
|
||||
capturedError = e;
|
||||
onError?.call(e);
|
||||
},
|
||||
);
|
||||
await cache.ready;
|
||||
if (latest != null) return latest!;
|
||||
throw capturedError ?? Exception('No data and no error from getChat');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
||||
|
||||
import '../../../infrastructure/loadableState/loading_error.dart';
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../repository/chat_list_repository.dart';
|
||||
@@ -7,6 +8,14 @@ import 'chat_list_event.dart';
|
||||
import 'chat_list_state.dart';
|
||||
|
||||
class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, ChatListRepository> {
|
||||
bool _forceRenew = false;
|
||||
|
||||
@override
|
||||
void retry() {
|
||||
_forceRenew = true;
|
||||
super.retry();
|
||||
}
|
||||
|
||||
@override
|
||||
ChatListRepository repository() => ChatListRepository();
|
||||
|
||||
@@ -21,15 +30,39 @@ class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, Ch
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final rooms = await repo.data.getRooms();
|
||||
final renew = _forceRenew;
|
||||
_forceRenew = false;
|
||||
|
||||
Object? capturedError;
|
||||
final rooms = await repo.data.getRooms(
|
||||
renew: renew,
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
add(DataGathered((s) => s.copyWith(rooms: rooms)));
|
||||
_updateAppBadge(rooms);
|
||||
|
||||
if (capturedError != null) throw capturedError!;
|
||||
}
|
||||
|
||||
Future<void> refresh({bool renew = true}) async {
|
||||
final rooms = await repo.data.getRooms(renew: renew);
|
||||
add(RefetchStarted<ChatListState>());
|
||||
Object? capturedError;
|
||||
try {
|
||||
final rooms = await repo.data.getRooms(
|
||||
renew: renew,
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
add(DataGathered((s) => s.copyWith(rooms: rooms)));
|
||||
_updateAppBadge(rooms);
|
||||
} catch (e) {
|
||||
capturedError = e;
|
||||
}
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: capturedError.toString(),
|
||||
allowRetry: true,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createDirectChat(String invite) async {
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../../../api/marianumcloud/talk/room/getRoomCache.dart';
|
||||
import '../../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../../api/marianumcloud/talk/createRoom/createRoom.dart';
|
||||
import '../../../../../api/marianumcloud/talk/createRoom/createRoomParams.dart';
|
||||
|
||||
class ChatListDataProvider {
|
||||
Future<GetRoomResponse> getRooms({bool renew = false}) {
|
||||
final completer = Completer<GetRoomResponse>();
|
||||
GetRoomCache(
|
||||
Future<GetRoomResponse> getRooms({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
GetRoomResponse? latest;
|
||||
Object? capturedError;
|
||||
final cache = GetRoomCache(
|
||||
renew: renew,
|
||||
onUpdate: (data) {
|
||||
if (!completer.isCompleted) completer.complete(data);
|
||||
onUpdate: (data) => latest = data,
|
||||
onError: (e) {
|
||||
capturedError = e;
|
||||
onError?.call(e);
|
||||
},
|
||||
);
|
||||
return completer.future;
|
||||
await cache.ready;
|
||||
if (latest != null) return latest!;
|
||||
throw capturedError ?? Exception('No data and no error from getRooms');
|
||||
}
|
||||
|
||||
Future<void> createDirectRoom(String invite) =>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart';
|
||||
import '../../../infrastructure/loadableState/loading_error.dart';
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../repository/files_repository.dart';
|
||||
@@ -28,12 +30,14 @@ class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesReposi
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
add(RefetchStarted<FilesState>());
|
||||
final path = innerState?.currentPath ?? initialPath;
|
||||
await _query(path);
|
||||
}
|
||||
|
||||
Future<void> setPath(List<String> path) async {
|
||||
add(Emit((s) => s.copyWith(currentPath: path, listing: null)));
|
||||
add(RefetchStarted<FilesState>());
|
||||
await _query(path);
|
||||
}
|
||||
|
||||
@@ -45,8 +49,34 @@ class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesReposi
|
||||
|
||||
Future<void> _query(List<String> path) async {
|
||||
final pathString = path.isEmpty ? '/' : path.join('/');
|
||||
final listing = await repo.data.listFiles(pathString);
|
||||
|
||||
Object? capturedError;
|
||||
ListFilesResponse? listing;
|
||||
try {
|
||||
listing = await repo.data.listFiles(
|
||||
pathString,
|
||||
onCacheData: (cached) {
|
||||
// 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);
|
||||
add(Emit((s) => s.copyWith(listing: cached)));
|
||||
},
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
} catch (e) {
|
||||
capturedError = e;
|
||||
}
|
||||
|
||||
if (listing != null) {
|
||||
listing.files.removeWhere((file) => file.name.isEmpty || file.name == path.lastOrNull);
|
||||
add(DataGathered((s) => s.copyWith(listing: listing)));
|
||||
}
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: capturedError.toString(),
|
||||
allowRetry: true,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
|
||||
import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart';
|
||||
@@ -7,15 +5,30 @@ import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesRespo
|
||||
import '../../../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
|
||||
class FilesDataProvider {
|
||||
Future<ListFilesResponse> listFiles(String path) {
|
||||
final completer = Completer<ListFilesResponse>();
|
||||
ListFilesCache(
|
||||
/// Lists files at [path]. Cached payload is delivered via [onCacheData] as
|
||||
/// soon as it is read from disk, so callers can render stale data while the
|
||||
/// network call is still pending. The Future itself resolves once both the
|
||||
/// cache lookup and the network attempt have settled, throwing if no payload
|
||||
/// could be obtained at all.
|
||||
Future<ListFilesResponse> listFiles(
|
||||
String path, {
|
||||
void Function(ListFilesResponse)? onCacheData,
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
ListFilesResponse? latest;
|
||||
Object? capturedError;
|
||||
final cache = ListFilesCache(
|
||||
path: path,
|
||||
onUpdate: (data) {
|
||||
if (!completer.isCompleted) completer.complete(data);
|
||||
onUpdate: (data) => latest = data,
|
||||
onCacheData: onCacheData,
|
||||
onError: (e) {
|
||||
capturedError = e;
|
||||
onError?.call(e);
|
||||
},
|
||||
);
|
||||
return completer.future;
|
||||
await cache.ready;
|
||||
if (latest != null) return latest!;
|
||||
throw capturedError ?? Exception('No data and no error from listFiles');
|
||||
}
|
||||
|
||||
Future<void> createFolder(String fullPath) async {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../repository/marianum_dates_repository.dart';
|
||||
import 'marianum_dates_event.dart';
|
||||
import 'marianum_dates_state.dart';
|
||||
|
||||
class MarianumDatesBloc extends LoadableHydratedBloc<MarianumDatesEvent, MarianumDatesState, MarianumDatesRepository> {
|
||||
MarianumDatesBloc() {
|
||||
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() ?? [];
|
||||
|
||||
@override
|
||||
fromNothing() => const MarianumDatesState(showPastEvents: false, events: []);
|
||||
@override
|
||||
fromStorage(Map<String, dynamic> json) => MarianumDatesState.fromJson(json);
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final events = await repo.getEvents();
|
||||
add(DataGathered((state) => state.copyWith(events: events)));
|
||||
}
|
||||
@override
|
||||
repository() => MarianumDatesRepository();
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(state) => state.toJson();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
|
||||
import 'marianum_dates_state.dart';
|
||||
|
||||
sealed class MarianumDatesEvent extends LoadableHydratedBlocEvent<MarianumDatesState> {}
|
||||
|
||||
class SetPastEventsVisible extends MarianumDatesEvent {
|
||||
final bool shouldBeVisible;
|
||||
SetPastEventsVisible(this.shouldBeVisible);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
part 'marianum_dates_state.freezed.dart';
|
||||
part 'marianum_dates_state.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class MarianumDatesState with _$MarianumDatesState {
|
||||
const factory MarianumDatesState({
|
||||
required bool showPastEvents,
|
||||
required List<MarianumDate> events,
|
||||
}) = _MarianumDatesState;
|
||||
|
||||
factory MarianumDatesState.fromJson(Map<String, Object?> json) => _$MarianumDatesStateFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MarianumDate with _$MarianumDate {
|
||||
const factory MarianumDate({
|
||||
required String uid,
|
||||
required String title,
|
||||
required String? description,
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
required bool isAllDay,
|
||||
}) = _MarianumDate;
|
||||
|
||||
factory MarianumDate.fromJson(Map<String, Object?> json) => _$MarianumDateFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// 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';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MarianumDatesState implements DiagnosticableTreeMixin {
|
||||
|
||||
bool get showPastEvents; List<MarianumDate> get events;
|
||||
/// Create a copy of MarianumDatesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$MarianumDatesStateCopyWith<MarianumDatesState> get copyWith => _$MarianumDatesStateCopyWithImpl<MarianumDatesState>(this as MarianumDatesState, _$identity);
|
||||
|
||||
/// Serializes this MarianumDatesState to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder 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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,showPastEvents,const DeepCollectionEquality().hash(events));
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'MarianumDatesState(showPastEvents: $showPastEvents, events: $events)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $MarianumDatesStateCopyWith<$Res> {
|
||||
factory $MarianumDatesStateCopyWith(MarianumDatesState value, $Res Function(MarianumDatesState) _then) = _$MarianumDatesStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool showPastEvents, List<MarianumDate> events
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$MarianumDatesStateCopyWithImpl<$Res>
|
||||
implements $MarianumDatesStateCopyWith<$Res> {
|
||||
_$MarianumDatesStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final MarianumDatesState _self;
|
||||
final $Res Function(MarianumDatesState) _then;
|
||||
|
||||
/// 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(
|
||||
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>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [MarianumDatesState].
|
||||
extension MarianumDatesStatePatterns on MarianumDatesState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _MarianumDatesState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDatesState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _MarianumDatesState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDatesState():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _MarianumDatesState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDatesState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool showPastEvents, List<MarianumDate> events)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDatesState() when $default != null:
|
||||
return $default(_that.showPastEvents,_that.events);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool showPastEvents, List<MarianumDate> events) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDatesState():
|
||||
return $default(_that.showPastEvents,_that.events);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool showPastEvents, List<MarianumDate> events)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDatesState() when $default != null:
|
||||
return $default(_that.showPastEvents,_that.events);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _MarianumDatesState with DiagnosticableTreeMixin implements MarianumDatesState {
|
||||
const _MarianumDatesState({required this.showPastEvents, required final List<MarianumDate> events}): _events = events;
|
||||
factory _MarianumDatesState.fromJson(Map<String, dynamic> json) => _$MarianumDatesStateFromJson(json);
|
||||
|
||||
@override final bool showPastEvents;
|
||||
final List<MarianumDate> _events;
|
||||
@override List<MarianumDate> get events {
|
||||
if (_events is EqualUnmodifiableListView) return _events;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_events);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of MarianumDatesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$MarianumDatesStateCopyWith<_MarianumDatesState> get copyWith => __$MarianumDatesStateCopyWithImpl<_MarianumDatesState>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$MarianumDatesStateToJson(this, );
|
||||
}
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder 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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,showPastEvents,const DeepCollectionEquality().hash(_events));
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'MarianumDatesState(showPastEvents: $showPastEvents, events: $events)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$MarianumDatesStateCopyWith<$Res> implements $MarianumDatesStateCopyWith<$Res> {
|
||||
factory _$MarianumDatesStateCopyWith(_MarianumDatesState value, $Res Function(_MarianumDatesState) _then) = __$MarianumDatesStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool showPastEvents, List<MarianumDate> events
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$MarianumDatesStateCopyWithImpl<$Res>
|
||||
implements _$MarianumDatesStateCopyWith<$Res> {
|
||||
__$MarianumDatesStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _MarianumDatesState _self;
|
||||
final $Res Function(_MarianumDatesState) _then;
|
||||
|
||||
/// Create a copy of MarianumDatesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? showPastEvents = null,Object? events = null,}) {
|
||||
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>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MarianumDate implements DiagnosticableTreeMixin {
|
||||
|
||||
String get uid; String get title; String? get description; DateTime get start; DateTime get end; bool get isAllDay;
|
||||
/// Create a copy of MarianumDate
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$MarianumDateCopyWith<MarianumDate> get copyWith => _$MarianumDateCopyWithImpl<MarianumDate>(this as MarianumDate, _$identity);
|
||||
|
||||
/// Serializes this MarianumDate to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder 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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => 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)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $MarianumDateCopyWith<$Res> {
|
||||
factory $MarianumDateCopyWith(MarianumDate value, $Res Function(MarianumDate) _then) = _$MarianumDateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$MarianumDateCopyWithImpl<$Res>
|
||||
implements $MarianumDateCopyWith<$Res> {
|
||||
_$MarianumDateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final MarianumDate _self;
|
||||
final $Res Function(MarianumDate) _then;
|
||||
|
||||
/// 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(
|
||||
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
|
||||
as String?,start: null == start ? _self.start : start // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,end: null == end ? _self.end : end // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,isAllDay: null == isAllDay ? _self.isAllDay : isAllDay // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [MarianumDate].
|
||||
extension MarianumDatePatterns on MarianumDate {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _MarianumDate value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDate() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _MarianumDate value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDate():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _MarianumDate value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDate() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDate() when $default != null:
|
||||
return $default(_that.uid,_that.title,_that.description,_that.start,_that.end,_that.isAllDay);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDate():
|
||||
return $default(_that.uid,_that.title,_that.description,_that.start,_that.end,_that.isAllDay);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MarianumDate() when $default != null:
|
||||
return $default(_that.uid,_that.title,_that.description,_that.start,_that.end,_that.isAllDay);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _MarianumDate with DiagnosticableTreeMixin implements MarianumDate {
|
||||
const _MarianumDate({required this.uid, required this.title, required this.description, required this.start, required this.end, required this.isAllDay});
|
||||
factory _MarianumDate.fromJson(Map<String, dynamic> json) => _$MarianumDateFromJson(json);
|
||||
|
||||
@override final String uid;
|
||||
@override final String title;
|
||||
@override final String? description;
|
||||
@override final DateTime start;
|
||||
@override final DateTime end;
|
||||
@override final bool isAllDay;
|
||||
|
||||
/// Create a copy of MarianumDate
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$MarianumDateCopyWith<_MarianumDate> get copyWith => __$MarianumDateCopyWithImpl<_MarianumDate>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$MarianumDateToJson(this, );
|
||||
}
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder 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));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => 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)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$MarianumDateCopyWith<$Res> implements $MarianumDateCopyWith<$Res> {
|
||||
factory _$MarianumDateCopyWith(_MarianumDate value, $Res Function(_MarianumDate) _then) = __$MarianumDateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String uid, String title, String? description, DateTime start, DateTime end, bool isAllDay
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$MarianumDateCopyWithImpl<$Res>
|
||||
implements _$MarianumDateCopyWith<$Res> {
|
||||
__$MarianumDateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _MarianumDate _self;
|
||||
final $Res Function(_MarianumDate) _then;
|
||||
|
||||
/// Create a copy of MarianumDate
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? uid = null,Object? title = null,Object? description = freezed,Object? start = null,Object? end = null,Object? isAllDay = null,}) {
|
||||
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
|
||||
as String?,start: null == start ? _self.start : start // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,end: null == end ? _self.end : end // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,isAllDay: null == isAllDay ? _self.isAllDay : isAllDay // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,41 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'marianum_dates_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_MarianumDatesState _$MarianumDatesStateFromJson(Map<String, dynamic> json) =>
|
||||
_MarianumDatesState(
|
||||
showPastEvents: json['showPastEvents'] as bool,
|
||||
events: (json['events'] as List<dynamic>)
|
||||
.map((e) => MarianumDate.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MarianumDatesStateToJson(_MarianumDatesState instance) =>
|
||||
<String, dynamic>{
|
||||
'showPastEvents': instance.showPastEvents,
|
||||
'events': instance.events,
|
||||
};
|
||||
|
||||
_MarianumDate _$MarianumDateFromJson(Map<String, dynamic> json) =>
|
||||
_MarianumDate(
|
||||
uid: json['uid'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
start: DateTime.parse(json['start'] as String),
|
||||
end: DateTime.parse(json['end'] as String),
|
||||
isAllDay: json['isAllDay'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MarianumDateToJson(_MarianumDate instance) =>
|
||||
<String, dynamic>{
|
||||
'uid': instance.uid,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'start': instance.start.toIso8601String(),
|
||||
'end': instance.end.toIso8601String(),
|
||||
'isAllDay': instance.isAllDay,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:enough_icalendar/enough_icalendar.dart';
|
||||
|
||||
import '../bloc/marianum_dates_state.dart';
|
||||
|
||||
class MarianumDatesGetEvents {
|
||||
static const String url = 'https://public-cal.marianumlan.de/cal_public/ad4c5da8-7466-9c72-89cb-8b8d9a5cf26c';
|
||||
|
||||
final Dio _dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10).inMilliseconds,
|
||||
receiveTimeout: const Duration(seconds: 30).inMilliseconds,
|
||||
));
|
||||
|
||||
Future<List<MarianumDate>> run() async {
|
||||
final response = await _dio.get<String>(url);
|
||||
final body = response.data;
|
||||
if (body == null || body.isEmpty) return [];
|
||||
|
||||
final root = VComponent.parse(body);
|
||||
final calendar = root is VCalendar ? root : null;
|
||||
final source = calendar?.children ?? root.children;
|
||||
|
||||
final events = source.whereType<VEvent>().map(_toMarianumDate).whereType<MarianumDate>().toList();
|
||||
events.sort((a, b) => a.start.compareTo(b.start));
|
||||
return events;
|
||||
}
|
||||
|
||||
static MarianumDate? _toMarianumDate(VEvent e) {
|
||||
final start = e.start;
|
||||
if (start == null) return null;
|
||||
final end = e.end ?? start;
|
||||
final isAllDay = _isAllDay(start, end);
|
||||
return MarianumDate(
|
||||
uid: e.uid,
|
||||
title: e.summary ?? '',
|
||||
description: e.description,
|
||||
start: start,
|
||||
end: end,
|
||||
isAllDay: isAllDay,
|
||||
);
|
||||
}
|
||||
|
||||
static bool _isAllDay(DateTime start, DateTime end) {
|
||||
final startMidnight = start.hour == 0 && start.minute == 0 && start.second == 0;
|
||||
final endMidnight = end.hour == 0 && end.minute == 0 && end.second == 0;
|
||||
return startMidnight && endMidnight && end.difference(start).inHours % 24 == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import '../../../infrastructure/repository/repository.dart';
|
||||
import '../bloc/marianum_dates_state.dart';
|
||||
import '../dataProvider/marianum_dates_get_events.dart';
|
||||
|
||||
class MarianumDatesRepository extends Repository<MarianumDatesState> {
|
||||
Future<List<MarianumDate>> getEvents() => MarianumDatesGetEvents().run();
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import 'dart:developer';
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
import '../../../../../storage/base/settings.dart';
|
||||
import '../../../../../view/settings/defaultSettings.dart';
|
||||
import '../../../../../storage/settings.dart';
|
||||
import '../../../../../view/pages/settings/data/default_settings.dart';
|
||||
import '../../app_modules.dart';
|
||||
|
||||
class SettingsCubit extends HydratedCubit<Settings> {
|
||||
static const _debounceTag = 'settings_persist';
|
||||
@@ -36,16 +37,28 @@ class SettingsCubit extends HydratedCubit<Settings> {
|
||||
@override
|
||||
Settings fromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return Settings.fromJson(json);
|
||||
return _appendNewModules(Settings.fromJson(json));
|
||||
} catch (_) {
|
||||
try {
|
||||
return Settings.fromJson(_mergeSettings(json, DefaultSettings.get().toJson()));
|
||||
return _appendNewModules(Settings.fromJson(_mergeSettings(json, DefaultSettings.get().toJson())));
|
||||
} catch (_) {
|
||||
return DefaultSettings.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modules added in newer app versions won't appear in a previously persisted
|
||||
// moduleOrder. Append any enum value that is neither ordered nor hidden so it
|
||||
// becomes visible in the "Mehr" menu without forcing a full settings reset.
|
||||
Settings _appendNewModules(Settings s) {
|
||||
final order = s.modulesSettings.moduleOrder;
|
||||
final hidden = s.modulesSettings.hiddenModules;
|
||||
final missing = Modules.values.where((m) => !order.contains(m) && !hidden.contains(m));
|
||||
if (missing.isEmpty) return s;
|
||||
s.modulesSettings.moduleOrder = [...order, ...missing];
|
||||
return s;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(Settings state) => state.toJson();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsCac
|
||||
import '../../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsResponse.dart';
|
||||
import '../../../../../api/webuntis/queries/getTimetable/getTimetableCache.dart';
|
||||
import '../../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
|
||||
import '../../../../../model/accountData.dart';
|
||||
import '../../../../../model/account_data.dart';
|
||||
|
||||
class TimetableDataProvider {
|
||||
static final DateFormat _dateFormat = DateFormat('yyyyMMdd');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'devToolsSettings.g.dart';
|
||||
part 'dev_tools_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class DevToolsSettings {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'devToolsSettings.dart';
|
||||
part of 'dev_tools_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../view/pages/files/files.dart';
|
||||
|
||||
part 'fileSettings.g.dart';
|
||||
part 'file_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class FileSettings {
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'fileSettings.dart';
|
||||
part of 'file_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'fileViewSettings.g.dart';
|
||||
part 'file_view_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class FileViewSettings {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'fileViewSettings.dart';
|
||||
part of 'file_view_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'holidaysSettings.g.dart';
|
||||
part 'holidays_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class HolidaysSettings {
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'holidaysSettings.dart';
|
||||
part of 'holidays_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -2,7 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../state/app/modules/app_modules.dart';
|
||||
|
||||
part 'modulesSettings.g.dart';
|
||||
part 'modules_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class ModulesSettings {
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'modulesSettings.dart';
|
||||
part of 'modules_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -33,4 +33,5 @@ const _$ModulesEnumMap = {
|
||||
Modules.roomPlan: 'roomPlan',
|
||||
Modules.gradeAveragesCalculator: 'gradeAveragesCalculator',
|
||||
Modules.holidays: 'holidays',
|
||||
Modules.marianumDates: 'marianumDates',
|
||||
};
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'notificationSettings.g.dart';
|
||||
part 'notification_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class NotificationSettings {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'notificationSettings.dart';
|
||||
part of 'notification_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../devTools/devToolsSettings.dart';
|
||||
import '../file/fileSettings.dart';
|
||||
import '../fileView/fileViewSettings.dart';
|
||||
import '../general/modulesSettings.dart';
|
||||
import '../holidays/holidaysSettings.dart';
|
||||
import '../notification/notificationSettings.dart';
|
||||
import '../talk/talkSettings.dart';
|
||||
import '../timetable/timetableSettings.dart';
|
||||
import 'dev_tools_settings.dart';
|
||||
import 'file_settings.dart';
|
||||
import 'file_view_settings.dart';
|
||||
import 'modules_settings.dart';
|
||||
import 'holidays_settings.dart';
|
||||
import 'notification_settings.dart';
|
||||
import 'talk_settings.dart';
|
||||
import 'timetable_settings.dart';
|
||||
|
||||
part 'settings.g.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'talkSettings.g.dart';
|
||||
part 'talk_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class TalkSettings {
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'talkSettings.dart';
|
||||
part of 'talk_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'timetable_name_mode.dart';
|
||||
import '../../../view/pages/timetable/data/timetable_name_mode.dart';
|
||||
|
||||
part 'timetableSettings.g.dart';
|
||||
part 'timetable_settings.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class TimetableSettings {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timetableSettings.dart';
|
||||
part of 'timetable_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widget/dropdownDisplay.dart';
|
||||
import '../widget/dropdown_display.dart';
|
||||
|
||||
class AppTheme {
|
||||
static DropdownDisplay getDisplayOptions(ThemeMode theme) {
|
||||
@@ -7,7 +7,7 @@ import 'package:flutter_login/flutter_login.dart';
|
||||
|
||||
import '../../api/marianumcloud/talk/room/getRoom.dart';
|
||||
import '../../api/marianumcloud/talk/room/getRoomParams.dart';
|
||||
import '../../model/accountData.dart';
|
||||
import '../../model/account_data.dart';
|
||||
import '../../state/app/modules/account/bloc/account_bloc.dart';
|
||||
import '../../state/app/modules/account/bloc/account_state.dart';
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import '../../../state/app/infrastructure/utilityWidgets/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 '../../../widget/filePick.dart';
|
||||
import '../../../widget/placeholderView.dart';
|
||||
import 'fileElement.dart';
|
||||
import 'filesUploadDialog.dart';
|
||||
import '../../../widget/file_pick.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import 'widgets/file_element.dart';
|
||||
import 'files_upload_dialog.dart';
|
||||
|
||||
class BetterSortOption {
|
||||
String displayName;
|
||||
@@ -164,9 +164,9 @@ class _FilesViewState extends State<_FilesView> {
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: LoadableStateConsumer<FilesBloc, FilesState>(
|
||||
isReady: (state) => state.listing != null,
|
||||
child: (state, _) {
|
||||
final listing = state.listing;
|
||||
if (listing == null) return const SizedBox.shrink();
|
||||
final listing = state.listing!;
|
||||
if (listing.files.isEmpty) {
|
||||
return const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer');
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
import '../../../widget/confirmDialog.dart';
|
||||
import '../../../widget/focusBehaviour.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import '../../../widget/focus_behaviour.dart';
|
||||
|
||||
class FilesUploadDialog extends StatefulWidget {
|
||||
final List<String> filePaths;
|
||||
+14
-18
@@ -1,23 +1,24 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flowder/flowder.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import '../../../widget/infoDialog.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
|
||||
import '../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
import '../../../model/endpointData.dart';
|
||||
import '../../../widget/centeredLeading.dart';
|
||||
import '../../../widget/confirmDialog.dart';
|
||||
import '../../../widget/fileViewer.dart';
|
||||
import '../../../widget/unimplementedDialog.dart';
|
||||
import 'files.dart';
|
||||
import '../../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
|
||||
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../model/endpoint_data.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/unimplemented_dialog.dart';
|
||||
|
||||
class FileElement extends StatefulWidget {
|
||||
final CacheableFile file;
|
||||
@@ -41,18 +42,15 @@ class FileElement extends StatefulWidget {
|
||||
file: File(local),
|
||||
progress: ProgressImplementation(),
|
||||
deleteOnCancel: true,
|
||||
client: Dio(BaseOptions(headers: AccountData().authHeaders())),
|
||||
onDone: () {
|
||||
//Future<OpenResult> result = OpenFile.open(local); // TODO legacy - refactor: remove onDone parameter
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => FileViewer(path: local)));
|
||||
AppRoutes.openFileViewer(context, local);
|
||||
onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
|
||||
// result.then((value) => {
|
||||
// onDone(value)
|
||||
// });
|
||||
},
|
||||
);
|
||||
|
||||
return await Flowder.download(
|
||||
'${await WebdavApi.webdavConnectString}$encodedPath',
|
||||
'${WebdavApi.buildWebdavUrl()}$encodedPath',
|
||||
options,
|
||||
);
|
||||
}
|
||||
@@ -98,9 +96,7 @@ class _FileElementState extends State<FileElement> {
|
||||
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
|
||||
onTap: () {
|
||||
if(widget.file.isDirectory) {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => Files(path: widget.path.toList()..add(widget.file.name)),
|
||||
));
|
||||
AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name));
|
||||
} else {
|
||||
if(EndpointData().getEndpointMode() == EndpointMode.stage) {
|
||||
InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../bloc/grade_averages_bloc.dart';
|
||||
import '../bloc/grade_averages_event.dart';
|
||||
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart';
|
||||
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart';
|
||||
|
||||
class GradeAveragesListView extends StatelessWidget {
|
||||
const GradeAveragesListView({super.key});
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../widget/confirmDialog.dart';
|
||||
import '../bloc/grade_averages_bloc.dart';
|
||||
import '../bloc/grade_averages_event.dart';
|
||||
import '../bloc/grade_averages_state.dart';
|
||||
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_bloc.dart';
|
||||
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_event.dart';
|
||||
import '../../../state/app/modules/gradeAverages/bloc/grade_averages_state.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import 'grade_averages_list_view.dart';
|
||||
|
||||
class GradeAveragesView extends StatelessWidget {
|
||||
+11
-11
@@ -1,17 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../../../../../widget/animatedTime.dart';
|
||||
import '../../../../../widget/list_view_util.dart';
|
||||
import '../../../../../widget/centeredLeading.dart';
|
||||
import '../../../../../widget/debug/debugTile.dart';
|
||||
import '../../../../../widget/string_extensions.dart';
|
||||
import '../../../infrastructure/loadableState/loadable_state.dart';
|
||||
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../infrastructure/utilityWidgets/bloc_module.dart';
|
||||
import '../bloc/holidays_bloc.dart';
|
||||
import '../bloc/holidays_event.dart';
|
||||
import '../bloc/holidays_state.dart';
|
||||
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/holidays/bloc/holidays_bloc.dart';
|
||||
import '../../../state/app/modules/holidays/bloc/holidays_event.dart';
|
||||
import '../../../state/app/modules/holidays/bloc/holidays_state.dart';
|
||||
import '../../../widget/animated_time.dart';
|
||||
import '../../../widget/centered_leading.dart';
|
||||
import '../../../widget/debug/debug_tile.dart';
|
||||
import '../../../widget/list_view_util.dart';
|
||||
import '../../../widget/string_extensions.dart';
|
||||
|
||||
class HolidaysView extends StatelessWidget {
|
||||
const HolidaysView({super.key});
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/marianumDates/bloc/marianum_dates_bloc.dart';
|
||||
import '../../../state/app/modules/marianumDates/bloc/marianum_dates_event.dart';
|
||||
import '../../../state/app/modules/marianumDates/bloc/marianum_dates_state.dart';
|
||||
import '../../../widget/animated_time.dart';
|
||||
import '../../../widget/centered_leading.dart';
|
||||
import '../../../widget/debug/debug_tile.dart';
|
||||
import '../../../widget/list_view_util.dart';
|
||||
import '../timetable/custom_events/custom_event_edit_dialog.dart';
|
||||
|
||||
class MarianumDatesView extends StatelessWidget {
|
||||
const MarianumDatesView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocModule<MarianumDatesBloc, LoadableState<MarianumDatesState>>(
|
||||
create: (context) => MarianumDatesBloc(),
|
||||
autoRebuild: true,
|
||||
child: (context, bloc, state) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Marianum Termine'),
|
||||
actions: [
|
||||
PopupMenuButton<bool>(
|
||||
initialValue: bloc.showPastEvents(),
|
||||
icon: const Icon(Icons.history),
|
||||
itemBuilder: (context) => [true, false].map((e) => PopupMenuItem<bool>(
|
||||
value: e,
|
||||
enabled: e != bloc.showPastEvents(),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(e ? Icons.history_outlined : Icons.history_toggle_off_outlined, color: Theme.of(context).colorScheme.onSurface),
|
||||
const SizedBox(width: 15),
|
||||
Text(e ? 'Alle anzeigen' : 'Nur zukünftige anzeigen'),
|
||||
],
|
||||
),
|
||||
)).toList(),
|
||||
onSelected: (e) => bloc.add(SetPastEventsVisible(e)),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LoadableStateConsumer<MarianumDatesBloc, MarianumDatesState>(
|
||||
child: (state, loading) => ListViewUtil.fromList<MarianumDate>(bloc.getEvents(), (event) => _MarianumDateTile(event: event)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _MarianumDateTile extends StatelessWidget {
|
||||
final MarianumDate event;
|
||||
const _MarianumDateTile({required this.event});
|
||||
|
||||
String _formatSubtitle() {
|
||||
final start = Jiffy.parseFromDateTime(event.start);
|
||||
final end = Jiffy.parseFromDateTime(event.end);
|
||||
|
||||
if (event.isAllDay) {
|
||||
// iCal end is exclusive for multi-day all-day events. The feed sets
|
||||
// DTSTART == DTEND for single-day all-day events, so only subtract a
|
||||
// day when end actually advances past start.
|
||||
final inclusiveEnd = event.end.isAfter(event.start) ? end.subtract(days: 1) : end;
|
||||
final sameAllDay = start.format(pattern: 'yyyy-MM-dd') == inclusiveEnd.format(pattern: 'yyyy-MM-dd');
|
||||
return sameAllDay
|
||||
? '${start.format(pattern: 'dd.MM.yyyy')} · Ganztägig'
|
||||
: '${start.format(pattern: 'dd.MM.yyyy')} – ${inclusiveEnd.format(pattern: 'dd.MM.yyyy')} · Ganztägig';
|
||||
}
|
||||
|
||||
final sameDay = start.format(pattern: 'yyyy-MM-dd') == end.format(pattern: 'yyyy-MM-dd');
|
||||
if (sameDay) {
|
||||
if (event.start == event.end) {
|
||||
return '${start.format(pattern: 'dd.MM.yyyy')} · ${start.format(pattern: 'HH:mm')}';
|
||||
}
|
||||
return '${start.format(pattern: 'dd.MM.yyyy')} · ${start.format(pattern: 'HH:mm')} – ${end.format(pattern: 'HH:mm')}';
|
||||
}
|
||||
return '${start.format(pattern: 'dd.MM.yyyy HH:mm')} – ${end.format(pattern: 'dd.MM.yyyy HH:mm')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.event)),
|
||||
title: Text(event.title.isEmpty ? '(ohne Titel)' : event.title),
|
||||
subtitle: Text(_formatSubtitle()),
|
||||
onTap: () => _showDetails(context),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
tooltip: 'In Stundenplan übernehmen',
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => CustomEventEditDialog(
|
||||
initialTitle: event.title,
|
||||
initialDescription: event.description,
|
||||
initialStart: event.start,
|
||||
initialEnd: event.end,
|
||||
),
|
||||
barrierDismissible: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void _showDetails(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: Text(event.title.isEmpty ? '(ohne Titel)' : event.title),
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.date_range_outlined)),
|
||||
title: Text(_formatSubtitle()),
|
||||
),
|
||||
if (event.description != null && event.description!.trim().isNotEmpty)
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.notes_outlined)),
|
||||
title: Text(event.description!.trim()),
|
||||
),
|
||||
Visibility(
|
||||
visible: !event.start.difference(DateTime.now()).isNegative,
|
||||
replacement: ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.content_paste_search_outlined)),
|
||||
title: Text(Jiffy.parseFromDateTime(event.start).fromNow()),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.timer_outlined)),
|
||||
title: AnimatedTime(callback: () => event.start.difference(DateTime.now())),
|
||||
subtitle: Text(Jiffy.parseFromDateTime(event.start).fromNow()),
|
||||
),
|
||||
),
|
||||
DebugTile(context).jsonData(event.toJson()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'marianum_message_view.dart';
|
||||
import '../../../infrastructure/loadableState/loadable_state.dart';
|
||||
import '../../../infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../infrastructure/utilityWidgets/bloc_module.dart';
|
||||
import '../bloc/marianum_message_bloc.dart';
|
||||
import '../bloc/marianum_message_state.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/marianumMessage/bloc/marianum_message_bloc.dart';
|
||||
import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
|
||||
|
||||
class MarianumMessageListView extends StatelessWidget {
|
||||
const MarianumMessageListView({super.key});
|
||||
@@ -31,7 +31,7 @@ class MarianumMessageListView extends StatelessWidget {
|
||||
subtitle: Text('vom ${message.date}'),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => MessageView(basePath: state.messageList.base, message: message)));
|
||||
AppRoutes.openMarianumMessage(context, state.messageList.base, message);
|
||||
},
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../bloc/marianum_message_state.dart';
|
||||
import '../../../../../widget/confirmDialog.dart';
|
||||
import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
|
||||
class MessageView extends StatefulWidget {
|
||||
final String basePath;
|
||||
+4
-4
@@ -11,11 +11,11 @@ import 'package:badges/badges.dart' as badges;
|
||||
|
||||
import '../../../../api/mhsl/server/feedback/addFeedback.dart';
|
||||
import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart';
|
||||
import '../../../../model/accountData.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/filePick.dart';
|
||||
import '../../../../widget/focusBehaviour.dart';
|
||||
import '../../../../widget/infoDialog.dart';
|
||||
import '../../../../widget/file_pick.dart';
|
||||
import '../../../../widget/focus_behaviour.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
|
||||
class FeedbackDialog extends StatefulWidget {
|
||||
const FeedbackDialog({super.key});
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
import 'appSharePlatformView.dart';
|
||||
import 'app_share_platform_view.dart';
|
||||
|
||||
class QrShareView extends StatefulWidget {
|
||||
const QrShareView({super.key});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user