Skip to content

Manual tagging of files #492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a2cf390
chore: update pubspec
aince42 Jan 7, 2025
44985e4
chore: add translations
aince42 Jan 7, 2025
5c4791a
chore: add example for tag collections
aince42 Jan 7, 2025
d8cc59e
chore: reload files on pop
aince42 Jan 7, 2025
a96d0d1
refactor: outsource file infos
aince42 Jan 7, 2025
9b012a2
feat: implement tagging and updating of tags
aince42 Jan 7, 2025
781ce74
chore: update file detail screen
aince42 Jan 7, 2025
3e8c72a
Merge branch 'main' into manual-tagging-of-files
aince42 Jan 7, 2025
a8f5c0f
fix: linter errors
aince42 Jan 8, 2025
c9068d6
fix: missing translations
aince42 Jan 8, 2025
5a1fb05
fix: error on add new file
aince42 Jan 8, 2025
8d0e579
chore: add comment for fallback
aince42 Jan 8, 2025
dc67a5c
fix: late initialization error
aince42 Jan 8, 2025
b9b75dd
refactor: simplify _getAvailableTagsData()
Siolto Jan 9, 2025
3dc57f8
Merge branch 'main' into manual-tagging-of-files
aince42 Jan 15, 2025
9790a03
Merge branch 'tagging-of-files' into manual-tagging-of-files
mergify[bot] Jan 16, 2025
2996d20
fix: add missing translation
aince42 Jan 20, 2025
8b0cbc7
chore: update dependencies
aince42 Jan 20, 2025
72fdae2
chore: remove unused textfield
aince42 Jan 20, 2025
a25c1c9
chore: make check in file detail screen
aince42 Jan 20, 2025
eb30eed
Merge remote-tracking branch 'origin/main' into manual-tagging-of-files
aince42 Jan 24, 2025
1988aa6
fix: imports
aince42 Jan 24, 2025
45b5c06
chore: update tagCollection to single level tags example
aince42 Jan 27, 2025
4c5422e
chore: update translations
aince42 Jan 27, 2025
5868982
chore: update assets path
aince42 Jan 27, 2025
3a94ea9
chore: update ui and logic for dispaying and editing tags
aince42 Jan 27, 2025
a66bb9a
chore: use real tag collection data
aince42 Jan 28, 2025
2ebbb8d
chore: unfo lockfile changes
jkoenig134 Feb 3, 2025
a7bc731
Merge branch 'tagging-of-files' into manual-tagging-of-files
jkoenig134 Feb 3, 2025
97c935c
Merge branch 'tagging-of-files' into manual-tagging-of-files
jkoenig134 Feb 3, 2025
09aa81c
refactor: remove useless late initializations
aince42 Feb 3, 2025
01de942
refactor: add barrel export
aince42 Feb 4, 2025
2adcc35
chore: update FileDetailScreen
aince42 Feb 4, 2025
a3c52c3
refactor: make one line
aince42 Feb 4, 2025
8c3b5f4
refactor: add _LoadingIndicator
aince42 Feb 4, 2025
5c3df57
Merge branch 'tagging-of-files' into manual-tagging-of-files
aince42 Feb 14, 2025
05f0652
Merge branch 'tagging-of-files' into manual-tagging-of-files
mergify[bot] Mar 18, 2025
32966ab
chore: add maxHeight for bottom sheet
aince42 Mar 31, 2025
3a0d95f
feat: add check for multi stage tags
aince42 Mar 31, 2025
ac789d2
feat: handle multi stage tags
aince42 Mar 31, 2025
284fdcd
feat: display selected tags
aince42 Mar 31, 2025
13f6afa
Merge branch 'tagging-of-files' into multi-stage-tagging
mergify[bot] Mar 31, 2025
0270110
refactor: use spacing
aince42 Apr 1, 2025
aee62d1
refactor: import
aince42 Apr 1, 2025
54835ee
Merge branch 'tagging-of-files' into multi-stage-tagging
mergify[bot] Apr 8, 2025
d7c1d21
Merge branch 'main' into multi-stage-tagging
mergify[bot] Apr 8, 2025
bfff19f
Merge branch 'main' into multi-stage-tagging
mergify[bot] Apr 8, 2025
9c337b3
Merge branch 'main' into multi-stage-tagging
mergify[bot] Apr 8, 2025
bc067a0
chore: undo podfile
aince42 Apr 8, 2025
25986f3
Merge branch 'main' into multi-stage-tagging
mergify[bot] Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 108 additions & 90 deletions apps/enmeshed/lib/account/my_data/file/file_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:enmeshed_ui_kit/enmeshed_ui_kit.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart';

import '/core/core.dart';
import 'modals/edit_file.dart';
import 'widgets/widgets.dart';

class FileDetailScreen extends StatefulWidget {
final String accountId;
Expand All @@ -20,131 +21,123 @@ class FileDetailScreen extends StatefulWidget {
}

class _FileDetailScreenState extends State<FileDetailScreen> {
FileDVO? _fileDVO;
late final Session _session;

late FileDVO _fileDVO;

LocalAttributeDVO? _fileReferenceAttribute;
List<String>? _tags;
AttributeTagCollectionDTO? _tagCollection;

bool _isLoadingFile = false;
bool _isOpeningFile = false;

@override
void initState() {
super.initState();

_session = GetIt.I.get<EnmeshedRuntime>().getSession(widget.accountId);

_fileDVO = widget.preLoadedFile;
_fileReferenceAttribute = widget.fileReferenceAttribute;
_tags = widget.fileReferenceAttribute?.tags;

if (_fileDVO == null) _load();
_load();
}

@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(title: Text(_fileDVO!.title, style: Theme.of(context).textTheme.titleLarge)),
appBar: AppBar(title: Text(_fileDVO.title, style: Theme.of(context).textTheme.titleLarge)),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 8, left: 24, right: 24),
child:
_fileDVO == null
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
FileIcon(filename: _fileDVO!.filename, color: Theme.of(context).colorScheme.primaryContainer, size: 40),
Gaps.h8,
Text(_fileDVO!.filename, style: Theme.of(context).textTheme.labelLarge),
Text('${bytesText(context: context, bytes: _fileDVO!.filesize)} - ${getFileExtension(_fileDVO!.filename)}'),
],
),
),
Gaps.h24,
if (_tags != null)
Chip(
label: Text(_tags!.join(', '), style: TextStyle(color: Theme.of(context).colorScheme.onSecondaryContainer)),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)),
side: BorderSide(color: Theme.of(context).colorScheme.secondaryContainer),
),
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
padding: EdgeInsets.zero,
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
),
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${context.l10n.files_owner}: ',
style: Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
'${context.l10n.files_createdAt}: ',
style: Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
Gaps.w24,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.i18nTranslate(_fileDVO!.createdBy.name), style: Theme.of(context).textTheme.bodyMedium),
Text(context.i18nTranslate(_formatDate(context, _fileDVO!.createdAt)), style: Theme.of(context).textTheme.bodyMedium),
],
),
],
),
Gaps.h32,
Row(
children: [
Gaps.w8,
IconButton(
onPressed: _isLoadingFile || DateTime.parse(_fileDVO!.expiresAt).isBefore(DateTime.now()) ? null : _downloadAndSaveFile,
icon:
_isLoadingFile
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator())
: const Icon(Icons.file_download, size: 24),
),
Gaps.w8,
IconButton(
onPressed: _isOpeningFile || DateTime.parse(_fileDVO!.expiresAt).isBefore(DateTime.now()) ? null : _openFile,
icon:
_isOpeningFile
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator())
: const Icon(Icons.open_with, size: 24),
),
],
),
],
padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
FileIcon(filename: _fileDVO.filename, color: Theme.of(context).colorScheme.primary, size: 40),
Gaps.h8,
Text(_fileDVO.filename, style: Theme.of(context).textTheme.labelLarge),
Text('${bytesText(context: context, bytes: _fileDVO.filesize)} - ${getFileExtension(_fileDVO.filename)}'),
],
),
),
Gaps.h48,
if (_isEditable) ...[
if (_tags == null || _tags!.isEmpty)
FileTagsContainer(onEditFile: _onEditFilePressed)
else
SelectedTagsSection(tagCollection: _tagCollection!, selectedTagsList: _tags!),
Gaps.h16,
],
FileInfoContainer(createdBy: _fileDVO.createdBy.name, createdAt: _fileDVO.createdAt),
Gaps.h32,
Row(
spacing: 8,
children: [
if (_isEditable) IconButton(onPressed: _onEditFilePressed, icon: const Icon(Icons.edit_outlined, size: 24)),
IconButton(
onPressed: _isLoadingFile || DateTime.parse(_fileDVO.expiresAt).isBefore(DateTime.now()) ? null : _downloadAndSaveFile,
icon: _isLoadingFile ? const _LoadingIndicator() : const Icon(Icons.file_download, size: 24),
),
IconButton(
onPressed: _isOpeningFile || DateTime.parse(_fileDVO.expiresAt).isBefore(DateTime.now()) ? null : _openFile,
icon: _isOpeningFile ? const _LoadingIndicator() : const Icon(Icons.open_with, size: 24),
),
],
),
],
),
),
),
);
}

String _formatDate(BuildContext context, String date) {
final locale = Localizations.localeOf(context);
final parsedDate = DateTime.parse(date).toLocal();
return DateFormat.yMd(locale.languageCode).format(parsedDate);
}
bool get _isEditable => _fileReferenceAttribute != null && _tagCollection != null;

Future<void> _load() async {
final session = GetIt.I.get<EnmeshedRuntime>().getSession(widget.accountId);
final response = await session.transportServices.files.getFile(fileId: widget.preLoadedFile.id);
final expanded = await session.expander.expandFileDTO(response.value);
await _loadFile();
await _loadTagCollection();
await _loadTags();
}

Future<void> _loadFile() async {
final response = await _session.transportServices.files.getFile(fileId: widget.preLoadedFile.id);
final expanded = await _session.expander.expandFileDTO(response.value);

setState(() => _fileDVO = expanded);
}

Future<void> _loadTagCollection() async {
final tagCollectionResult = await _session.consumptionServices.attributes.getAttributeTagCollection();

if (tagCollectionResult.isError) return;
setState(() => _tagCollection = tagCollectionResult.value);
}

Future<void> _loadTags({String? attributeId}) async {
if (_fileReferenceAttribute == null) return;

final response = await _session.consumptionServices.attributes.getAttribute(attributeId: attributeId ?? _fileReferenceAttribute!.id);
final expanded = await _session.expander.expandLocalAttributeDTO(response.value);

setState(() {
_tags = expanded.tags;
_fileReferenceAttribute = expanded;
});
}

Future<void> _downloadAndSaveFile() async {
setState(() => _isLoadingFile = true);

final session = GetIt.I.get<EnmeshedRuntime>().getSession(widget.accountId);
await moveFileOnDevice(
session: session,
fileDVO: _fileDVO!,
fileDVO: _fileDVO,
onError: () {
if (mounted) showDownloadFileErrorDialog(context);
},
Expand All @@ -159,12 +152,37 @@ class _FileDetailScreenState extends State<FileDetailScreen> {
final session = GetIt.I.get<EnmeshedRuntime>().getSession(widget.accountId);
await openFile(
session: session,
fileDVO: _fileDVO!,
fileDVO: _fileDVO,
onError: () {
if (mounted) showDownloadFileErrorDialog(context);
},
);

if (mounted) setState(() => _isOpeningFile = false);
}

void _onEditFilePressed() {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder:
(_) => ConstrainedBox(
constraints: BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height * 0.9),
child: EditFile(
accountId: widget.accountId,
fileTitle: _fileDVO.title,
fileReferenceAttribute: _fileReferenceAttribute!,
tagCollection: _tagCollection!,
onSave: _loadTags,
),
),
);
}
}

class _LoadingIndicator extends StatelessWidget {
const _LoadingIndicator();

@override
Widget build(BuildContext context) => const SizedBox(width: 24, height: 24, child: CircularProgressIndicator());
}
13 changes: 9 additions & 4 deletions apps/enmeshed/lib/account/my_data/file/files_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,12 @@ class _FilesScreenState extends State<FilesScreen> {
onRefresh: () => _loadFiles(syncBefore: true),
child: ListView.separated(
itemBuilder:
(context, index) =>
FileItem(accountId: widget.accountId, fileRecord: _filteredFileRecords[index], trailing: const Icon(Icons.chevron_right)),
(context, index) => FileItem(
accountId: widget.accountId,
fileRecord: _filteredFileRecords[index],
trailing: const Icon(Icons.chevron_right),
reload: _loadFiles,
),
itemCount: _filteredFileRecords.length,
separatorBuilder: (context, index) => const Divider(height: 2, indent: 16),
),
Expand Down Expand Up @@ -251,13 +255,14 @@ class _FilesScreenState extends State<FilesScreen> {
fileRecord: item,
query: keyword,
accountId: widget.accountId,
onTap: () {
onTap: () async {
controller
..clear()
..closeView(null);
FocusScope.of(context).unfocus();

context.push('/account/${widget.accountId}/my-data/files/${item.file.id}', extra: item);
await context.push('/account/${widget.accountId}/my-data/files/${item.file.id}', extra: item);
unawaited(_loadFiles());
},
),
);
Expand Down
Loading