claude refactorings, flutter best practices, platform dependent changes, general cleanup
This commit is contained in:
+218
-38
@@ -1,21 +1,26 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.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 '../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
||||
import '../../../api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
|
||||
import '../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/files/bloc/files_bloc.dart';
|
||||
import '../../../state/app/modules/files/bloc/files_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../utils/cache_invalidation_bus.dart';
|
||||
import '../../../utils/file_clipboard.dart';
|
||||
import '../../../widget/async_action_button.dart';
|
||||
import '../../../widget/file_pick.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import 'widgets/file_element.dart';
|
||||
import 'files_upload_dialog.dart';
|
||||
import 'widgets/file_element.dart';
|
||||
|
||||
class BetterSortOption {
|
||||
String displayName;
|
||||
@@ -78,6 +83,11 @@ class _FilesViewState extends State<_FilesView> {
|
||||
late final SettingsCubit settings;
|
||||
late SortOption currentSort;
|
||||
late bool currentSortDirection;
|
||||
late final StreamSubscription<String> _invalidationSub;
|
||||
|
||||
// Cache key in FilesBloc's pathString format: '/' for root, otherwise
|
||||
// segments joined without leading/trailing slash.
|
||||
String get _myPathString => widget.path.isEmpty ? '/' : widget.path.join('/');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -85,12 +95,25 @@ class _FilesViewState extends State<_FilesView> {
|
||||
settings = context.read<SettingsCubit>();
|
||||
currentSort = settings.val().fileSettings.sortBy;
|
||||
currentSortDirection = settings.val().fileSettings.ascending;
|
||||
_invalidationSub = CacheInvalidationBus.listFilesStream.listen(_onInvalidation);
|
||||
}
|
||||
|
||||
void _onInvalidation(String invalidatedPath) {
|
||||
if (!mounted) return;
|
||||
if (invalidatedPath != _myPathString) return;
|
||||
context.read<FilesBloc>().refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_invalidationSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> mediaUpload(List<String>? paths) async {
|
||||
if (paths == null) return;
|
||||
final bloc = context.read<FilesBloc>();
|
||||
pushScreen(
|
||||
unawaited(pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
@@ -98,7 +121,7 @@ class _FilesViewState extends State<_FilesView> {
|
||||
remotePath: widget.path.join('/'),
|
||||
onUploadFinished: (_) => bloc.refresh(),
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -163,28 +186,39 @@ class _FilesViewState extends State<_FilesView> {
|
||||
onPressed: () => _showAddDialog(context, bloc),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: LoadableStateConsumer<FilesBloc, FilesState>(
|
||||
isReady: (state) => state.listing != null,
|
||||
child: (state, _) {
|
||||
final listing = state.listing!;
|
||||
if (listing.files.isEmpty) {
|
||||
return const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer');
|
||||
}
|
||||
final files = listing.sortBy(
|
||||
sortOption: currentSort,
|
||||
foldersToTop: context.watch<SettingsCubit>().val().fileSettings.sortFoldersToTop,
|
||||
reversed: currentSortDirection,
|
||||
);
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: files.length,
|
||||
itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh),
|
||||
);
|
||||
},
|
||||
body: Column(
|
||||
children: [
|
||||
_ClipboardBanner(currentFolder: _currentFolderPath, onPasteDone: bloc.refresh),
|
||||
Expanded(
|
||||
child: LoadableStateConsumer<FilesBloc, FilesState>(
|
||||
isReady: (state) => state.listing != null,
|
||||
child: (state, _) {
|
||||
final listing = state.listing!;
|
||||
if (listing.files.isEmpty) {
|
||||
return const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer');
|
||||
}
|
||||
final files = listing.sortBy(
|
||||
sortOption: currentSort,
|
||||
foldersToTop: context.watch<SettingsCubit>().val().fileSettings.sortFoldersToTop,
|
||||
reversed: currentSortDirection,
|
||||
);
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: files.length,
|
||||
itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Relative folder path matching the WebDAV format used by `CacheableFile.path`
|
||||
// (no leading slash; trailing slash for non-root). Empty string means root.
|
||||
String get _currentFolderPath => widget.path.isEmpty ? '' : '${widget.path.join('/')}/';
|
||||
|
||||
void _showAddDialog(BuildContext context, FilesBloc bloc) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -205,18 +239,25 @@ class _FilesViewState extends State<_FilesView> {
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: !Platform.isIOS,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.add_a_photo_outlined),
|
||||
title: const Text('Aus Gallerie hochladen'),
|
||||
onTap: () {
|
||||
FilePick.multipleGalleryPick().then((value) {
|
||||
if (value != null) mediaUpload(value.map((e) => e.path).toList());
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_a_photo_outlined),
|
||||
title: const Text('Aus Galerie hochladen'),
|
||||
onTap: () {
|
||||
FilePick.multipleGalleryPick().then((value) {
|
||||
if (value != null) mediaUpload(value.map((e) => e.path).toList());
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt_outlined),
|
||||
title: const Text('Foto aufnehmen'),
|
||||
onTap: () {
|
||||
FilePick.cameraPick().then((image) {
|
||||
if (image != null) mediaUpload([image.path]);
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
]),
|
||||
);
|
||||
@@ -248,3 +289,142 @@ class _FilesViewState extends State<_FilesView> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClipboardBanner extends StatefulWidget {
|
||||
const _ClipboardBanner({required this.currentFolder, required this.onPasteDone});
|
||||
final String currentFolder;
|
||||
final void Function() onPasteDone;
|
||||
|
||||
@override
|
||||
State<_ClipboardBanner> createState() => _ClipboardBannerState();
|
||||
}
|
||||
|
||||
class _ClipboardBannerState extends State<_ClipboardBanner> {
|
||||
bool _busy = false;
|
||||
|
||||
// All paths here are relative to the WebDAV root (matching `CacheableFile.path`).
|
||||
// Root is the empty string ''. Folders end with '/'.
|
||||
String _normalised(String path) {
|
||||
final stripped = path.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
return stripped.isEmpty ? '' : '$stripped/';
|
||||
}
|
||||
|
||||
String _joinPath(String folder, String name, {required bool isDirectory}) =>
|
||||
isDirectory ? '$folder$name/' : '$folder$name';
|
||||
|
||||
// Disabled when:
|
||||
// - clipboard is empty
|
||||
// - we'd be pasting a folder into itself or one of its descendants
|
||||
// - every entry already lives in the current folder (paste would be a no-op)
|
||||
bool get _canPaste {
|
||||
final cb = FileClipboard.instance;
|
||||
if (cb.isEmpty) return false;
|
||||
final dst = _normalised(widget.currentFolder);
|
||||
var atLeastOneActionable = false;
|
||||
for (final f in cb.files) {
|
||||
if (f.isDirectory) {
|
||||
final src = _normalised(f.path);
|
||||
if (dst == src || dst.startsWith(src)) return false;
|
||||
}
|
||||
final destination = _joinPath(widget.currentFolder, f.name, isDirectory: f.isDirectory);
|
||||
if (destination != f.path) atLeastOneActionable = true;
|
||||
}
|
||||
return atLeastOneActionable;
|
||||
}
|
||||
|
||||
// Cache key format used by ListFilesCache (matches FilesBloc's pathString:
|
||||
// relative, no leading or trailing slash; root is '/').
|
||||
String _parentCacheKey(String relativePath) {
|
||||
final stripped = relativePath.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
if (!stripped.contains('/')) return '/';
|
||||
final parts = stripped.split('/')..removeLast();
|
||||
return parts.isEmpty ? '/' : parts.join('/');
|
||||
}
|
||||
|
||||
Future<void> _paste() async {
|
||||
final cb = FileClipboard.instance;
|
||||
if (_busy || !_canPaste) return;
|
||||
setState(() => _busy = true);
|
||||
final operation = cb.operation;
|
||||
final errors = <String>[];
|
||||
final invalidatedSourceFolders = <String>{};
|
||||
try {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
for (final file in cb.files) {
|
||||
final destination = _joinPath(widget.currentFolder, file.name, isDirectory: file.isDirectory);
|
||||
if (destination == file.path) continue;
|
||||
try {
|
||||
if (operation == FileClipboardOperation.cut) {
|
||||
await webdav.move(PathUri.parse(file.path), PathUri.parse(destination));
|
||||
invalidatedSourceFolders.add(_parentCacheKey(file.path));
|
||||
} else {
|
||||
await webdav.copy(PathUri.parse(file.path), PathUri.parse(destination));
|
||||
}
|
||||
} on Object catch (e) {
|
||||
errors.add('${file.name}: $e');
|
||||
}
|
||||
}
|
||||
// After cut, the source folders no longer contain the moved files. Drop
|
||||
// their cached listings so the next visit fetches fresh data instead of
|
||||
// briefly showing the moved file as still present.
|
||||
for (final folder in invalidatedSourceFolders) {
|
||||
await ListFilesCache.invalidate(folder);
|
||||
}
|
||||
if (operation == FileClipboardOperation.cut) cb.clear();
|
||||
widget.onPasteDone();
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
if (errors.isNotEmpty && mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Einfügen teilweise fehlgeschlagen'),
|
||||
content: SingleChildScrollView(child: Text(errors.join('\n\n'))),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListenableBuilder(
|
||||
listenable: FileClipboard.instance,
|
||||
builder: (context, _) {
|
||||
final cb = FileClipboard.instance;
|
||||
if (cb.isEmpty) return const SizedBox.shrink();
|
||||
final cut = cb.operation == FileClipboardOperation.cut;
|
||||
final count = cb.files.length;
|
||||
final label = count == 1 ? '"${cb.files.first.name}"' : '$count Elemente';
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(cut ? Icons.drive_file_move_outline : Icons.copy_outlined, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
cut ? '$label verschieben' : '$label kopieren',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _busy || !_canPaste ? null : _paste,
|
||||
child: _busy
|
||||
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('Hier einfügen'),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Verwerfen',
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: _busy ? null : cb.clear,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loader_overlay/loader_overlay.dart';
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
import '../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import '../../../widget/focus_behaviour.dart';
|
||||
|
||||
@@ -107,14 +106,14 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
_showUploadError('Verbindung fehlgeschlagen: $e');
|
||||
return;
|
||||
}
|
||||
var conflictingFiles = _uploadableFiles.where((file) {
|
||||
var fileName = file.fileName;
|
||||
return result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName'));
|
||||
final conflictingFiles = _uploadableFiles.where((file) {
|
||||
final fileName = file.fileName;
|
||||
return result.any((element) => Uri.decodeComponent((element as WebDavResponse).href!).endsWith('/$fileName'));
|
||||
}).toList();
|
||||
|
||||
if(conflictingFiles.isNotEmpty) {
|
||||
if (conflictingFiles.isNotEmpty) {
|
||||
if (!mounted) return;
|
||||
bool replaceFiles = await showDialog(
|
||||
final replaceFiles = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -160,7 +159,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
)
|
||||
);
|
||||
|
||||
if(!replaceFiles) {
|
||||
if (replaceFiles != true) {
|
||||
setState(() {
|
||||
_isUploading = false;
|
||||
_overallProgressValue = 0.0;
|
||||
@@ -179,7 +178,10 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
var fileName = file.fileName;
|
||||
var filePath = file.filePath;
|
||||
|
||||
if(widget.uniqueNames) fileName = '${fileName.split('.').first}-${const Uuid().v4()}.${fileName.split('.').last}';
|
||||
if (widget.uniqueNames) {
|
||||
final unique = DateTime.now().microsecondsSinceEpoch.toRadixString(36);
|
||||
fileName = '${fileName.split('.').first}-$unique.${fileName.split('.').last}';
|
||||
}
|
||||
|
||||
var fullRemotePath = '${widget.remotePath}/$fileName';
|
||||
|
||||
@@ -187,7 +189,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
|
||||
_infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}';
|
||||
});
|
||||
|
||||
final dynamic uploadTask;
|
||||
final HttpClientResponse uploadTask;
|
||||
try {
|
||||
uploadTask = await webdavClient.putFile(
|
||||
File(filePath),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
||||
|
||||
/// Shows a modal bottom sheet with technical metadata about a single file or
|
||||
/// folder: full path, MIME type, size, timestamps, ETag.
|
||||
Future<void> showFileDetailsSheet(BuildContext context, CacheableFile file) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (context) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(file.isDirectory ? Icons.folder : Icons.description_outlined, size: 32),
|
||||
title: Text(file.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(file.isDirectory ? 'Ordner' : (file.mimeType ?? '–')),
|
||||
),
|
||||
const Divider(),
|
||||
_DetailRow(label: 'Pfad', value: file.path, copyable: true),
|
||||
if (!file.isDirectory) _DetailRow(label: 'Größe', value: filesize(file.size)),
|
||||
if (file.modifiedAt != null)
|
||||
_DetailRow(
|
||||
label: 'Geändert',
|
||||
value: '${Jiffy.parseFromDateTime(file.modifiedAt!).format(pattern: 'dd.MM.yyyy HH:mm')} '
|
||||
'(${Jiffy.parseFromDateTime(file.modifiedAt!).fromNow()})',
|
||||
),
|
||||
if (file.createdAt != null)
|
||||
_DetailRow(
|
||||
label: 'Erstellt',
|
||||
value: Jiffy.parseFromDateTime(file.createdAt!).format(pattern: 'dd.MM.yyyy HH:mm'),
|
||||
),
|
||||
if (file.eTag != null) _DetailRow(label: 'ETag', value: file.eTag!, copyable: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _DetailRow extends StatelessWidget {
|
||||
const _DetailRow({required this.label, required this.value, this.copyable = false});
|
||||
final String label;
|
||||
final String value;
|
||||
final bool copyable;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
),
|
||||
Expanded(child: SelectableText(value)),
|
||||
if (copyable)
|
||||
IconButton(
|
||||
tooltip: 'Kopieren',
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: value));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('In Zwischenablage kopiert')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,18 @@
|
||||
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/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/account_data.dart';
|
||||
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
||||
import '../../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../../../model/endpoint_data.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../utils/download_manager.dart';
|
||||
import '../../../../utils/file_clipboard.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/unimplemented_dialog.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
import 'file_details_sheet.dart';
|
||||
|
||||
class FileElement extends StatefulWidget {
|
||||
final CacheableFile file;
|
||||
@@ -26,57 +20,117 @@ class FileElement extends StatefulWidget {
|
||||
final void Function() refetch;
|
||||
const FileElement(this.file, this.path, this.refetch, {super.key});
|
||||
|
||||
static Future<DownloaderCore> download(BuildContext context, String remotePath, String name, Function(double) onProgress, Function(OpenResult) onDone) async {
|
||||
var paths = await getTemporaryDirectory();
|
||||
|
||||
var encodedPath = Uri.encodeComponent(remotePath);
|
||||
encodedPath = encodedPath.replaceAll('%2F', '/');
|
||||
|
||||
var local = paths.path + Platform.pathSeparator + name;
|
||||
|
||||
var options = DownloaderUtils(
|
||||
progressCallback: (current, total) {
|
||||
final progress = (current / total) * 100;
|
||||
onProgress(progress);
|
||||
},
|
||||
file: File(local),
|
||||
progress: ProgressImplementation(),
|
||||
deleteOnCancel: true,
|
||||
client: Dio(BaseOptions(headers: AccountData().authHeaders())),
|
||||
onDone: () {
|
||||
AppRoutes.openFileViewer(context, local);
|
||||
onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
|
||||
},
|
||||
);
|
||||
|
||||
return await Flowder.download(
|
||||
'${WebdavApi.buildWebdavUrl()}$encodedPath',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<FileElement> createState() => _FileElementState();
|
||||
}
|
||||
|
||||
class _FileElementState extends State<FileElement> {
|
||||
double percent = 0;
|
||||
Future<DownloaderCore>? downloadCore;
|
||||
DownloadJob? _job;
|
||||
|
||||
Widget getSubtitle() {
|
||||
if(widget.file.currentlyDownloading) {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_attachJob(DownloadManager.instance.jobFor(widget.file.path));
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FileElement oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.file.path != widget.file.path) {
|
||||
_detachJob();
|
||||
_attachJob(DownloadManager.instance.jobFor(widget.file.path));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_detachJob();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _attachJob(DownloadJob? job) {
|
||||
_job = job;
|
||||
if (job == null) return;
|
||||
job.status.addListener(_onStatusChange);
|
||||
if (job.isFinished) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onStatusChange());
|
||||
}
|
||||
}
|
||||
|
||||
void _detachJob() {
|
||||
_job?.status.removeListener(_onStatusChange);
|
||||
_job = null;
|
||||
}
|
||||
|
||||
void _onStatusChange() {
|
||||
if (!mounted) return;
|
||||
final job = _job;
|
||||
if (job == null) return;
|
||||
final status = job.status.value;
|
||||
if (status is DownloadDone) {
|
||||
DownloadManager.instance.clear(widget.file.path);
|
||||
_detachJob();
|
||||
AppRoutes.openFileViewer(context, status.localPath);
|
||||
setState(() {});
|
||||
} else if (status is DownloadFailed) {
|
||||
final message = status.message;
|
||||
DownloadManager.instance.clear(widget.file.path);
|
||||
_detachJob();
|
||||
setState(() {});
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Download'),
|
||||
content: Text(message),
|
||||
),
|
||||
);
|
||||
} else if (status is DownloadCancelled) {
|
||||
DownloadManager.instance.clear(widget.file.path);
|
||||
_detachJob();
|
||||
setState(() {});
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startDownload() async {
|
||||
final job = await DownloadManager.instance.start(
|
||||
remotePath: widget.file.path,
|
||||
name: widget.file.name,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (_job == job) return;
|
||||
_detachJob();
|
||||
_attachJob(job);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _confirmCancel() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => ConfirmDialog(
|
||||
title: 'Download abbrechen?',
|
||||
content: 'Möchtest du den Download abbrechen?',
|
||||
cancelButton: 'Nein',
|
||||
confirmButton: 'Ja, Abbrechen',
|
||||
onConfirm: () => _job?.cancel(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _subtitle() {
|
||||
final status = _job?.status.value;
|
||||
if (status is DownloadInProgress) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
child: const Text('Download:'),
|
||||
),
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(value: percent/100),
|
||||
),
|
||||
Expanded(child: LinearProgressIndicator(value: status.percent / 100)),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 10),
|
||||
child: Text('${percent.round()}%'),
|
||||
child: Text('${status.percent.round()}%'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -86,101 +140,173 @@ class _FileElementState extends State<FileElement> {
|
||||
: Text('${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}');
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (widget.file.isDirectory) {
|
||||
AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name));
|
||||
return;
|
||||
}
|
||||
if (EndpointData().getEndpointMode() == EndpointMode.stage) {
|
||||
InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
|
||||
return;
|
||||
}
|
||||
final status = _job?.status.value;
|
||||
if (status is DownloadInProgress) {
|
||||
_confirmCancel();
|
||||
return;
|
||||
}
|
||||
_startDownload();
|
||||
}
|
||||
|
||||
// All paths here are relative to the WebDAV root (matching CacheableFile.path).
|
||||
// Root parent is the empty string ''. Folders end with '/'.
|
||||
String _parentPathOf(String path) {
|
||||
final stripped = path.replaceAll(RegExp(r'^/+|/+$'), '');
|
||||
if (!stripped.contains('/')) return '';
|
||||
final parts = stripped.split('/')..removeLast();
|
||||
return parts.isEmpty ? '' : '${parts.join('/')}/';
|
||||
}
|
||||
|
||||
String _joinPath(String folder, String name, {required bool isDirectory}) =>
|
||||
isDirectory ? '$folder$name/' : '$folder$name';
|
||||
|
||||
Future<void> _rename() async {
|
||||
final controller = TextEditingController(text: widget.file.name);
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: const Text('Umbenennen'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(labelText: 'Neuer Name'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Abbrechen')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogCtx).pop(controller.text.trim()),
|
||||
child: const Text('Umbenennen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (newName == null || newName.isEmpty || newName == widget.file.name) return;
|
||||
|
||||
final parent = _parentPathOf(widget.file.path);
|
||||
final destination = _joinPath(parent, newName, isDirectory: widget.file.isDirectory);
|
||||
await _runWebdavOp(() async {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
await webdav.move(PathUri.parse(widget.file.path), PathUri.parse(destination));
|
||||
}, errorTitle: 'Umbenennen fehlgeschlagen');
|
||||
}
|
||||
|
||||
void _putOnClipboard({required bool copy}) {
|
||||
if (copy) {
|
||||
FileClipboard.instance.copy([widget.file]);
|
||||
} else {
|
||||
FileClipboard.instance.cut([widget.file]);
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('"${widget.file.name}" zum ${copy ? "Kopieren" : "Verschieben"} bereitgelegt'),
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ConfirmDialog(
|
||||
title: 'Element löschen?',
|
||||
content: 'Das Element wird unwiederruflich gelöscht.',
|
||||
confirmButton: 'Löschen',
|
||||
onConfirmAsync: () async {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
await webdav.delete(PathUri.parse(widget.file.path));
|
||||
widget.refetch();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runWebdavOp(Future<void> Function() action, {required String errorTitle}) async {
|
||||
try {
|
||||
await action();
|
||||
widget.refetch();
|
||||
} on Object catch (e) {
|
||||
if (!mounted) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: Text(errorTitle),
|
||||
content: Text(e.toString()),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showActionSheet() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetCtx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.info_outline)),
|
||||
title: const Text('Info'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
showFileDetailsSheet(context, widget.file);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.drive_file_rename_outline)),
|
||||
title: const Text('Umbenennen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_rename();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.drive_file_move_outline)),
|
||||
title: const Text('Verschieben'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_putOnClipboard(copy: false);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.copy_outlined)),
|
||||
title: const Text('Kopieren'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_putOnClipboard(copy: true);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CenteredLeading(Icon(Icons.delete_outline)),
|
||||
title: const Text('Löschen'),
|
||||
onTap: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
_delete();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
leading: CenteredLeading(
|
||||
Icon(widget.file.isDirectory ? Icons.folder : Icons.description_outlined)
|
||||
),
|
||||
title: Text(widget.file.name, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
subtitle: getSubtitle(),
|
||||
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
|
||||
onTap: () {
|
||||
if(widget.file.isDirectory) {
|
||||
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!');
|
||||
return;
|
||||
}
|
||||
if(widget.file.currentlyDownloading) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ConfirmDialog(
|
||||
title: 'Download abbrechen?',
|
||||
content: 'Möchtest du den Download abbrechen?',
|
||||
cancelButton: 'Nein',
|
||||
confirmButton: 'Ja, Abbrechen',
|
||||
onConfirm: () {
|
||||
downloadCore?.then((value) {
|
||||
if(!value.isCancelled) value.cancel();
|
||||
});
|
||||
setState(() {
|
||||
widget.file.currentlyDownloading = false;
|
||||
percent = 0;
|
||||
downloadCore = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
widget.file.currentlyDownloading = true;
|
||||
});
|
||||
|
||||
downloadCore = FileElement.download(context, widget.file.path, widget.file.name, (progress) {
|
||||
setState(() => percent = progress);
|
||||
}, (result) {
|
||||
if(result.type != ResultType.done) {
|
||||
showDialog(context: context, builder: (context) => AlertDialog(
|
||||
title: const Text('Download'),
|
||||
content: Text(result.message),
|
||||
));
|
||||
}
|
||||
|
||||
setState(() {
|
||||
widget.file.currentlyDownloading = false;
|
||||
percent = 0;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
showDialog(context: context, builder: (context) => SimpleDialog(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Löschen'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showDialog(context: context, builder: (context) => ConfirmDialog(
|
||||
title: 'Element löschen?',
|
||||
content: 'Das Element wird unwiederruflich gelöscht.',
|
||||
confirmButton: 'Löschen',
|
||||
onConfirmAsync: () async {
|
||||
final webdav = await WebdavApi.webdav;
|
||||
await webdav.delete(PathUri.parse(widget.file.path));
|
||||
widget.refetch();
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: !kReleaseMode,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.share_outlined),
|
||||
title: const Text('Teilen'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
UnimplementedDialog.show(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
},
|
||||
);
|
||||
leading: CenteredLeading(
|
||||
Icon(widget.file.isDirectory ? Icons.folder : Icons.description_outlined),
|
||||
),
|
||||
title: Text(widget.file.name, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
subtitle: _subtitle(),
|
||||
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
|
||||
onTap: _onTap,
|
||||
onLongPress: _showActionSheet,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/grade_averages/bloc/grade_averages_bloc.dart';
|
||||
import '../../../state/app/modules/grade_averages/bloc/grade_averages_event.dart';
|
||||
|
||||
class GradeAveragesListView extends StatelessWidget {
|
||||
const GradeAveragesListView({super.key});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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 '../../../state/app/modules/grade_averages/bloc/grade_averages_bloc.dart';
|
||||
import '../../../state/app/modules/grade_averages/bloc/grade_averages_event.dart';
|
||||
import '../../../state/app/modules/grade_averages/bloc/grade_averages_state.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import 'grade_averages_list_view.dart';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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/infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/holidays/bloc/holidays_bloc.dart';
|
||||
import '../../../state/app/modules/holidays/bloc/holidays_event.dart';
|
||||
import '../../../state/app/modules/holidays/bloc/holidays_state.dart';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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 '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/marianum_dates/bloc/marianum_dates_bloc.dart';
|
||||
import '../../../state/app/modules/marianum_dates/bloc/marianum_dates_event.dart';
|
||||
import '../../../state/app/modules/marianum_dates/bloc/marianum_dates_state.dart';
|
||||
import '../../../widget/animated_time.dart';
|
||||
import '../../../widget/centered_leading.dart';
|
||||
import '../../../widget/debug/debug_tile.dart';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.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';
|
||||
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/marianum_message/bloc/marianum_message_bloc.dart';
|
||||
import '../../../state/app/modules/marianum_message/bloc/marianum_message_state.dart';
|
||||
|
||||
class MarianumMessageListView extends StatelessWidget {
|
||||
const MarianumMessageListView({super.key});
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../state/app/modules/marianumMessage/bloc/marianum_message_state.dart';
|
||||
import '../../../state/app/modules/marianum_message/bloc/marianum_message_state.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
|
||||
class MessageView extends StatefulWidget {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:loader_overlay/loader_overlay.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
|
||||
import '../../../../api/mhsl/server/feedback/addFeedback.dart';
|
||||
import '../../../../api/mhsl/server/feedback/addFeedbackParams.dart';
|
||||
import '../../../../api/mhsl/server/feedback/add_feedback.dart';
|
||||
import '../../../../api/mhsl/server/feedback/add_feedback_params.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/file_pick.dart';
|
||||
@@ -129,7 +130,8 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
context.loaderOverlay.show();
|
||||
var imageData = await (await FilePick.galleryPick())?.readAsBytes();
|
||||
final picked = await FilePick.multipleGalleryPick();
|
||||
final imageData = await picked?.first.readAsBytes();
|
||||
if(context.mounted) context.loaderOverlay.hide();
|
||||
setState(() {
|
||||
_image = imageData;
|
||||
@@ -148,26 +150,26 @@ class _FeedbackDialogState extends State<FeedbackDialog> {
|
||||
return;
|
||||
}
|
||||
context.loaderOverlay.show();
|
||||
AddFeedback(
|
||||
unawaited(AddFeedback(
|
||||
AddFeedbackParams(
|
||||
user: AccountData().getUserSecret(),
|
||||
feedback: _feedbackInput.text,
|
||||
screenshot: _image != null ? base64Encode(_image!) : null,
|
||||
appVersion: int.parse((await PackageInfo.fromPlatform()).buildNumber),
|
||||
)
|
||||
),
|
||||
).run().then((value) {
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
InfoDialog.show(context, 'Danke für dein Feedback!');
|
||||
context.loaderOverlay.hide();
|
||||
}).catchError((error, trace) {
|
||||
}).catchError((Object error, StackTrace trace) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = error.toString();
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
context.loaderOverlay.hide();
|
||||
});
|
||||
}));
|
||||
},
|
||||
child: const Text('Senden'),
|
||||
)
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../extensions/render_not_null.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
|
||||
import '../../extensions/render_not_null.dart';
|
||||
import '../../routing/app_routes.dart';
|
||||
import '../../state/app/modules/app_modules.dart';
|
||||
import '../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../storage/settings.dart' as model;
|
||||
import '../../widget/centered_leading.dart';
|
||||
import '../../widget/info_dialog.dart';
|
||||
import 'settings/data/default_settings.dart';
|
||||
import 'more/share/select_share_type_dialog.dart';
|
||||
import 'settings/data/default_settings.dart';
|
||||
|
||||
class Overhang extends StatefulWidget {
|
||||
const Overhang({super.key});
|
||||
@@ -50,7 +50,11 @@ class _OverhangState extends State<Overhang> {
|
||||
final settings = context.read<SettingsCubit>();
|
||||
void changeVisibility(Modules module) {
|
||||
var hidden = settings.val(write: true).modulesSettings.hiddenModules;
|
||||
hidden.contains(module) ? hidden.remove(module) : (hidden.length < 3 ? hidden.add(module) : null);
|
||||
if (hidden.contains(module)) {
|
||||
hidden.remove(module);
|
||||
} else if (hidden.length < 3) {
|
||||
hidden.add(module);
|
||||
}
|
||||
}
|
||||
|
||||
return ReorderableListView(
|
||||
|
||||
@@ -3,16 +3,16 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../state/app/modules/app_modules.dart';
|
||||
import '../../../../storage/settings.dart';
|
||||
import '../../../../storage/dev_tools_settings.dart';
|
||||
import '../../../../storage/file_settings.dart';
|
||||
import '../../../../storage/file_view_settings.dart';
|
||||
import '../../../../storage/modules_settings.dart';
|
||||
import '../../../../storage/holidays_settings.dart';
|
||||
import '../../../../storage/modules_settings.dart';
|
||||
import '../../../../storage/notification_settings.dart';
|
||||
import '../../../../storage/settings.dart';
|
||||
import '../../../../storage/talk_settings.dart';
|
||||
import '../../../../view/pages/timetable/data/timetable_name_mode.dart';
|
||||
import '../../../../storage/timetable_settings.dart';
|
||||
import '../../../../view/pages/timetable/data/timetable_name_mode.dart';
|
||||
import '../../files/files.dart';
|
||||
|
||||
class DefaultSettings {
|
||||
|
||||
@@ -31,9 +31,10 @@ class AccountSection extends StatelessWidget {
|
||||
await prefs.clear();
|
||||
PaintingBinding.instance.imageCache.clear();
|
||||
if (!context.mounted) return;
|
||||
context.read<SettingsCubit>().reset();
|
||||
const CacheView().clear();
|
||||
AccountData().removeData(context: context);
|
||||
await context.read<SettingsCubit>().reset();
|
||||
await const CacheView().clear();
|
||||
if (!context.mounted) return;
|
||||
await AccountData().removeData(context: context);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
|
||||
@@ -3,20 +3,20 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_split_view/flutter_split_view.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/chatList/bloc/chat_list_bloc.dart';
|
||||
import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
|
||||
import '../../../notification/notify_updater.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
|
||||
import '../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../state/app/modules/chat_list/bloc/chat_list_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../widget/confirm_dialog.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
import 'widgets/chat_tile.dart';
|
||||
import 'widgets/split_view_placeholder.dart';
|
||||
import 'join_chat.dart';
|
||||
import 'search_chat.dart';
|
||||
import 'widgets/chat_tile.dart';
|
||||
import 'widgets/split_view_placeholder.dart';
|
||||
|
||||
class ChatList extends StatelessWidget {
|
||||
const ChatList({super.key});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../extensions/date_time.dart';
|
||||
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../state/app/modules/chat/bloc/chat_state.dart';
|
||||
import '../../../theming/app_theme.dart';
|
||||
import '../../../widget/clickable_app_bar.dart';
|
||||
import '../../../widget/user_avatar.dart';
|
||||
import 'details/chat_info.dart';
|
||||
import 'talk_navigator.dart';
|
||||
import 'widgets/chat_bubble.dart';
|
||||
import 'widgets/chat_textfield.dart';
|
||||
import 'talk_navigator.dart';
|
||||
|
||||
class ChatView extends StatefulWidget {
|
||||
final GetRoomResponseObject room;
|
||||
@@ -99,7 +99,7 @@ class _ChatViewState extends State<ChatView> {
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Container(
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: const AssetImage('assets/background/chat.png'),
|
||||
@@ -122,7 +122,7 @@ class _ChatViewState extends State<ChatView> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: TalkNavigator.isSecondaryVisible(context)
|
||||
? ChatTextfield(widget.room.token, selfId: widget.selfId)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:bubble/bubble.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../theming/app_theme.dart';
|
||||
import '../widgets/bubble.dart';
|
||||
|
||||
extension ColorExtensions on Color {
|
||||
Color invert() {
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../model/endpoint_data.dart';
|
||||
import '../../../../utils/url_opener.dart';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart';
|
||||
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_participants/get_participants_cache.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_participants/get_participants_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../widget/large_profile_picture_view.dart';
|
||||
import '../../../../widget/loading_spinner.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/getReactions/getReactions.dart';
|
||||
import '../../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_reactions/get_reactions.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_reactions/get_reactions_response.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/loading_spinner.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_participants/get_participants_response.dart';
|
||||
import '../../../../widget/user_avatar.dart';
|
||||
|
||||
class ParticipantsListView extends StatelessWidget {
|
||||
@@ -10,7 +10,7 @@ class ParticipantsListView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
lastname(participant) => participant.displayName.toString().split(' ').last;
|
||||
String lastname(participant) => participant.displayName.toString().split(' ').last;
|
||||
|
||||
final participants = participantsResponse.data
|
||||
.sorted((a, b) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import 'package:async/async.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../api/errors/error_mapper.dart';
|
||||
import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart';
|
||||
import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart';
|
||||
import '../../../api/marianumcloud/autocomplete/autocomplete_api.dart';
|
||||
import '../../../api/marianumcloud/autocomplete/autocomplete_response.dart';
|
||||
import '../../../model/endpoint_data.dart';
|
||||
import '../../../widget/app_progress_indicator.dart';
|
||||
import '../../../widget/placeholder_view.dart';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import 'widgets/chat_tile.dart';
|
||||
|
||||
class SearchChat extends SearchDelegate {
|
||||
class SearchChat extends SearchDelegate<GetRoomResponseObject?> {
|
||||
List<GetRoomResponseObject> chats;
|
||||
|
||||
SearchChat(this.chats);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
|
||||
import '../data/chat_bubble_styles.dart';
|
||||
|
||||
class AnswerReference extends StatelessWidget {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum BubbleNip { leftTop, rightBottom, none }
|
||||
|
||||
class BubbleEdges {
|
||||
const BubbleEdges.only({this.top = 0, this.bottom = 0, this.left = 0, this.right = 0});
|
||||
const BubbleEdges.all(double value)
|
||||
: top = value,
|
||||
bottom = value,
|
||||
left = value,
|
||||
right = value;
|
||||
|
||||
final double top;
|
||||
final double bottom;
|
||||
final double left;
|
||||
final double right;
|
||||
|
||||
EdgeInsets toEdgeInsets() => EdgeInsets.fromLTRB(left, top, right, bottom);
|
||||
}
|
||||
|
||||
class BubbleStyle {
|
||||
const BubbleStyle({
|
||||
this.color,
|
||||
this.borderWidth = 0,
|
||||
this.elevation = 0,
|
||||
this.margin = const BubbleEdges.only(),
|
||||
this.padding = const BubbleEdges.all(8),
|
||||
this.alignment = Alignment.centerLeft,
|
||||
this.nip = BubbleNip.none,
|
||||
this.borderRadius = 12,
|
||||
});
|
||||
|
||||
final Color? color;
|
||||
final double borderWidth;
|
||||
final double elevation;
|
||||
final BubbleEdges margin;
|
||||
final BubbleEdges padding;
|
||||
final Alignment alignment;
|
||||
final BubbleNip nip;
|
||||
final double borderRadius;
|
||||
}
|
||||
|
||||
/// Lightweight chat bubble. Replaces the abandoned `bubble` package: renders a
|
||||
/// rounded container with optional shadow / border. The nip is conveyed by
|
||||
/// flattening one corner so the bubble visually anchors to the speaker side.
|
||||
class Bubble extends StatelessWidget {
|
||||
const Bubble({required this.child, required this.style, super.key});
|
||||
|
||||
final Widget child;
|
||||
final BubbleStyle style;
|
||||
|
||||
BorderRadius _radius() {
|
||||
final r = Radius.circular(style.borderRadius);
|
||||
final flat = Radius.zero;
|
||||
switch (style.nip) {
|
||||
case BubbleNip.leftTop:
|
||||
return BorderRadius.only(topLeft: flat, topRight: r, bottomLeft: r, bottomRight: r);
|
||||
case BubbleNip.rightBottom:
|
||||
return BorderRadius.only(topLeft: r, topRight: r, bottomLeft: r, bottomRight: flat);
|
||||
case BubbleNip.none:
|
||||
return BorderRadius.all(r);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final radius = _radius();
|
||||
return Align(
|
||||
alignment: style.alignment,
|
||||
child: Container(
|
||||
margin: style.margin.toEdgeInsets(),
|
||||
decoration: BoxDecoration(
|
||||
color: style.color,
|
||||
borderRadius: radius,
|
||||
border: style.borderWidth > 0
|
||||
? Border.all(color: Theme.of(context).dividerColor, width: style.borderWidth)
|
||||
: null,
|
||||
boxShadow: style.elevation > 0
|
||||
? [BoxShadow(color: Colors.black26, blurRadius: style.elevation * 2, offset: Offset(0, style.elevation))]
|
||||
: null,
|
||||
),
|
||||
padding: style.padding.toEdgeInsets(),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
import 'package:bubble/bubble.dart';
|
||||
import 'package:flowder/flowder.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
|
||||
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/delete_react_message/delete_react_message.dart';
|
||||
import '../../../../api/marianumcloud/talk/delete_react_message/delete_react_message_params.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_poll/get_poll_state.dart';
|
||||
import '../../../../api/marianumcloud/talk/react_message/react_message.dart';
|
||||
import '../../../../api/marianumcloud/talk/react_message/react_message_params.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../extensions/text.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../utils/download_manager.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/loading_spinner.dart';
|
||||
import '../../files/widgets/file_element.dart';
|
||||
import '../data/chat_bubble_styles.dart';
|
||||
import '../data/chat_message.dart';
|
||||
import 'answer_reference.dart';
|
||||
import 'bubble.dart';
|
||||
import 'chat_message_options_dialog.dart';
|
||||
import 'poll_options_list.dart';
|
||||
|
||||
@@ -53,12 +52,95 @@ class ChatBubble extends StatefulWidget {
|
||||
|
||||
class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateMixin {
|
||||
late ChatMessage message;
|
||||
double downloadProgress = 0;
|
||||
Future<DownloaderCore>? downloadCore;
|
||||
DownloadJob? _job;
|
||||
|
||||
late Offset _position = const Offset(0, 0);
|
||||
late Offset _dragStartPosition = Offset.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final filePath = widget.bubbleData.messageParameters?['file']?.path;
|
||||
if (filePath != null) _attachJob(DownloadManager.instance.jobFor(filePath));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_detachJob();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _attachJob(DownloadJob? job) {
|
||||
_job = job;
|
||||
if (job == null) return;
|
||||
job.status.addListener(_onStatusChange);
|
||||
if (job.isFinished) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onStatusChange());
|
||||
}
|
||||
}
|
||||
|
||||
void _detachJob() {
|
||||
_job?.status.removeListener(_onStatusChange);
|
||||
_job = null;
|
||||
}
|
||||
|
||||
void _onStatusChange() {
|
||||
if (!mounted) return;
|
||||
final job = _job;
|
||||
if (job == null) return;
|
||||
final status = job.status.value;
|
||||
if (status is DownloadDone) {
|
||||
DownloadManager.instance.clear(job.remotePath);
|
||||
_detachJob();
|
||||
AppRoutes.openFileViewer(context, status.localPath);
|
||||
setState(() {});
|
||||
} else if (status is DownloadFailed) {
|
||||
final message = status.message;
|
||||
DownloadManager.instance.clear(job.remotePath);
|
||||
_detachJob();
|
||||
setState(() {});
|
||||
showDialog<void>(context: context, builder: (context) => AlertDialog(content: Text(message)));
|
||||
} else if (status is DownloadCancelled) {
|
||||
DownloadManager.instance.clear(job.remotePath);
|
||||
_detachJob();
|
||||
setState(() {});
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startFileDownload() async {
|
||||
final file = message.file;
|
||||
final filePath = file?.path;
|
||||
if (file == null || filePath == null) return;
|
||||
final job = await DownloadManager.instance.start(remotePath: filePath, name: file.name);
|
||||
if (!mounted) return;
|
||||
if (_job == job) return;
|
||||
_detachJob();
|
||||
_attachJob(job);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _confirmCancel() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Download abbrechen?'),
|
||||
content: const Text('Möchtest du den Download abbrechen?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Nein')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
_job?.cancel();
|
||||
},
|
||||
child: const Text('Ja, Abbrechen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BubbleStyle getStyle() {
|
||||
var styles = ChatBubbleStyles(context);
|
||||
if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) {
|
||||
@@ -162,53 +244,12 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
));
|
||||
}
|
||||
|
||||
if(message.file == null) return;
|
||||
|
||||
if(downloadProgress > 0) {
|
||||
showDialog(context: context, builder: (context) => AlertDialog(
|
||||
title: const Text('Download abbrechen?'),
|
||||
content: const Text('Möchtest du den Download abbrechen?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
}, child: const Text('Nein')),
|
||||
TextButton(onPressed: () {
|
||||
downloadCore?.then((value) {
|
||||
if(!value.isCancelled) value.cancel();
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
setState(() {
|
||||
downloadProgress = 0;
|
||||
downloadCore = null;
|
||||
});
|
||||
}, child: const Text('Ja, Abbrechen'))
|
||||
],
|
||||
));
|
||||
|
||||
return;
|
||||
if (message.file == null) return;
|
||||
if (_job?.status.value is DownloadInProgress) {
|
||||
_confirmCancel();
|
||||
} else {
|
||||
_startFileDownload();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
downloadProgress = 1;
|
||||
});
|
||||
downloadCore = FileElement.download(context, message.file!.path!, message.file!.name, (progress) {
|
||||
if(progress > 1) {
|
||||
setState(() {
|
||||
downloadProgress = progress;
|
||||
});
|
||||
}
|
||||
}, (result) {
|
||||
setState(() {
|
||||
downloadProgress = 0;
|
||||
});
|
||||
|
||||
if(result.type != ResultType.done) {
|
||||
showDialog(context: context, builder: (context) => AlertDialog(
|
||||
content: Text(result.message),
|
||||
));
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: _position,
|
||||
@@ -270,15 +311,18 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
|
||||
)
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: downloadProgress > 0,
|
||||
child: Positioned(
|
||||
if (_job?.status.value is DownloadInProgress)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: LinearProgressIndicator(value: downloadProgress == 1 ? null : downloadProgress/100),
|
||||
child: LinearProgressIndicator(
|
||||
value: () {
|
||||
final s = _job!.status.value as DownloadInProgress;
|
||||
return s.percent <= 0 ? null : s.percent / 100;
|
||||
}(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/actions/talk_actions.dart';
|
||||
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/react_message/react_message.dart';
|
||||
import '../../../../api/marianumcloud/talk/react_message/react_message_params.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../widget/app_progress_indicator.dart';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -5,11 +6,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart';
|
||||
import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/sendMessage/sendMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
import '../../../../api/marianumcloud/files_sharing/file_sharing_api.dart';
|
||||
import '../../../../api/marianumcloud/files_sharing/file_sharing_api_params.dart';
|
||||
import '../../../../api/marianumcloud/talk/send_message/send_message.dart';
|
||||
import '../../../../api/marianumcloud/talk/send_message/send_message_params.dart';
|
||||
import '../../../../api/marianumcloud/webdav/webdav_api.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
@@ -51,10 +52,10 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
if (paths == null) return;
|
||||
|
||||
const shareFolder = 'MarianumMobile';
|
||||
WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')));
|
||||
unawaited(WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder'))));
|
||||
|
||||
if (!mounted) return;
|
||||
pushScreen(
|
||||
unawaited(pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
@@ -63,7 +64,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
|
||||
uniqueNames: true,
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
void _setDraft(String text) {
|
||||
|
||||
@@ -5,13 +5,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/actions/talk_actions.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart';
|
||||
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/set_read_marker/set_read_marker.dart';
|
||||
import '../../../../api/marianumcloud/talk/set_read_marker/set_read_marker_params.dart';
|
||||
import '../../../../model/account_data.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
|
||||
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/get_poll/get_poll_state_response.dart';
|
||||
import '../../../../utils/url_opener.dart';
|
||||
|
||||
class PollOptionsList extends StatefulWidget {
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:jiffy/jiffy.dart';
|
||||
import 'package:rrule_generator/rrule_generator.dart';
|
||||
import 'package:time_range_picker/time_range_picker.dart';
|
||||
|
||||
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../widget/focus_behaviour.dart';
|
||||
@@ -151,6 +151,7 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
selectedColor: Theme.of(context).primaryColor,
|
||||
ticks: 24,
|
||||
);
|
||||
if (range is! TimeRange) return;
|
||||
setState(() {
|
||||
_startTime = range.startTime;
|
||||
_endTime = range.endTime;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
|
||||
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../api/webuntis/queries/get_timetable/get_timetable_response.dart';
|
||||
|
||||
sealed class ArbitraryAppointment {
|
||||
const ArbitraryAppointment();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/webuntis/queries/getTimegridUnits/getTimegridUnitsResponse.dart';
|
||||
import '../../../../api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
|
||||
class LessonPeriod {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
|
||||
import '../../../../api/webuntis/queries/get_timetable/get_timetable_response.dart';
|
||||
|
||||
enum LessonStatus {
|
||||
cancelled,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
|
||||
import '../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
|
||||
import '../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
|
||||
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../api/webuntis/queries/get_rooms/get_rooms_response.dart';
|
||||
import '../../../../api/webuntis/queries/get_subjects/get_subjects_response.dart';
|
||||
import '../../../../api/webuntis/queries/get_timetable/get_timetable_response.dart';
|
||||
import '../../../../storage/timetable_settings.dart';
|
||||
import 'timetable_name_mode.dart';
|
||||
import '../custom_events/custom_event_colors.dart';
|
||||
import 'arbitrary_appointment.dart';
|
||||
import 'lesson_color.dart';
|
||||
import 'lesson_status.dart';
|
||||
import 'timetable_name_mode.dart';
|
||||
import 'webuntis_time.dart';
|
||||
|
||||
class TimetableAppointmentFactory {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showAppointmentBottomSheet(
|
||||
BuildContext context, {
|
||||
required Widget Function(BuildContext context) header,
|
||||
required SliverChildListDelegate Function(BuildContext context) body,
|
||||
}) {
|
||||
showStickyFlexibleBottomSheet(
|
||||
minHeight: 0,
|
||||
initHeight: 0.4,
|
||||
maxHeight: 0.7,
|
||||
anchors: [0, 0.4, 0.7],
|
||||
isSafeArea: true,
|
||||
maxHeaderHeight: 100,
|
||||
context: context,
|
||||
headerBuilder: (context, _) => header(context),
|
||||
bodyBuilder: (context, _) => body(context),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showAppointmentBottomSheet(
|
||||
BuildContext context, {
|
||||
required Widget Function(BuildContext context) header,
|
||||
required SliverChildListDelegate Function(BuildContext context) body,
|
||||
}) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (sheetContext) => DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.4,
|
||||
minChildSize: 0.2,
|
||||
maxChildSize: 0.7,
|
||||
snap: true,
|
||||
snapSizes: const [0.4],
|
||||
builder: (_, scrollController) => CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _StickyHeader(child: header(sheetContext)),
|
||||
),
|
||||
SliverList(delegate: body(sheetContext)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _StickyHeader extends SliverPersistentHeaderDelegate {
|
||||
_StickyHeader({required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
double get minExtent => 100;
|
||||
@override
|
||||
double get maxExtent => 100;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SizedBox.expand(child: child),
|
||||
);
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant _StickyHeader oldDelegate) => oldDelegate.child != child;
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:rrule/rrule.dart';
|
||||
|
||||
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../widget/centered_leading.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../custom_events/custom_event_edit_dialog.dart';
|
||||
import '_bottom_sheet.dart';
|
||||
import 'bottom_sheet.dart';
|
||||
import 'delete_custom_event.dart';
|
||||
|
||||
class CustomEventSheet {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../api/mhsl/customTimetableEvent/customTimetableEvent.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
import '../../../../api/webuntis/queries/getRooms/getRoomsResponse.dart';
|
||||
import '../../../../api/webuntis/queries/getSubjects/getSubjectsResponse.dart';
|
||||
import '../../../../api/webuntis/queries/getTimetable/getTimetableResponse.dart';
|
||||
import '../../../../api/webuntis/queries/get_rooms/get_rooms_response.dart';
|
||||
import '../../../../api/webuntis/queries/get_subjects/get_subjects_response.dart';
|
||||
import '../../../../api/webuntis/queries/get_timetable/get_timetable_response.dart';
|
||||
import '../../../../routing/app_routes.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../../../../widget/debug/debug_tile.dart';
|
||||
import '../../../../widget/unimplemented_dialog.dart';
|
||||
import '_bottom_sheet.dart';
|
||||
import 'bottom_sheet.dart';
|
||||
|
||||
class WebuntisLessonSheet {
|
||||
static void show(BuildContext context, TimetableBloc bloc, Appointment appointment, GetTimetableResponseObject lesson) {
|
||||
|
||||
@@ -4,10 +4,10 @@ import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
import '../../../extensions/date_time.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../storage/timetable_settings.dart';
|
||||
import 'custom_events/custom_event_edit_dialog.dart';
|
||||
import 'data/arbitrary_appointment.dart';
|
||||
|
||||
@@ -56,7 +56,7 @@ class AppointmentTile extends StatelessWidget {
|
||||
),
|
||||
if (crossedOut)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(width: 2, color: Colors.red.withAlpha(200)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
|
||||
@@ -392,7 +392,7 @@ class _PeriodLabel extends StatelessWidget {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final showTimes = constraints.maxHeight >= 38;
|
||||
return Container(
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: dividerColor, width: 0.5)),
|
||||
),
|
||||
@@ -561,7 +561,7 @@ class _DayColumn extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPressStart: (details) => _handleLongPress(details, dayAppointments),
|
||||
child: Container(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? theme.colorScheme.primary.withAlpha(14) : null,
|
||||
border: Border(left: BorderSide(color: theme.dividerColor.withAlpha(90), width: 0.5)),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
import '../../../../api/webuntis/queries/getHolidays/getHolidaysResponse.dart';
|
||||
import '../../../../api/webuntis/queries/get_holidays/get_holidays_response.dart';
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../data/calendar_layout.dart';
|
||||
import '../data/lesson_period_schedule.dart';
|
||||
|
||||
Reference in New Issue
Block a user