Skip to content
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

Manual tagging of files – single-level tags #377

Open
wants to merge 35 commits into
base: tagging-of-files
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
35 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
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
113 changes: 113 additions & 0 deletions apps/enmeshed/assets/tag_example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
"supportedLanguages": ["de", "en"],
"tagsForAttributeValueTypes": {
"IdentityFileReference": {
"schulabschluss": {
"displayNames": {
"de": "Abschluss",
"en": "Degree"
},
"children": {
"realschule": {
"displayNames": {
"de": "Realschule",
"en": "Secondary School"
},
"children": {
"zeugnis": {
"displayNames": {
"de": "Zeugnis",
"en": "Diploma"
}
}
}
},
"gymnasium": {
"displayNames": {
"de": "Gymnasium",
"en": "High School"
},
"children": {
"zeugnis": {
"displayNames": {
"de": "Zeugnis",
"en": "Diploma"
}
},
"abitur": {
"displayNames": {
"de": "Abitur",
"en": "A-Level Certificate"
}
}
}
}
}
},
"versicherungsunterlagen": {
"displayNames": {
"de": "Versicherungsunterlagen",
"en": "Insurance documents"
},
"children": {
"haftpflichtversicherung": {
"displayNames": {
"de": "Haftpflichtversicherung",
"en": "Liability Insurance"
},
"children": {
"police": {
"displayNames": {
"de": "Versicherungspolice",
"en": "Insurance Policy"
}
}
}
},
"krankenversicherung": {
"displayNames": {
"de": "Krankenversicherung",
"en": "Health Insurance"
},
"children": {
"karte": {
"displayNames": {
"de": "Versicherungskarte",
"en": "Insurance Card"
}
},
"nachweise": {
"displayNames": {
"de": "Nachweise",
"en": "Proofs"
}
}
}
}
}
}
},
"PhoneNumber": {
"notfall": {
"displayNames": {
"de": "Notfallkontakt",
"en": "Emergency Contact"
}
}
},
"StreetAddress": {
"lieferung": {
"displayNames": {
"de": "Lieferadresse",
"en": "Deliver Address"
}
},
"heimat": {
"displayNames": {
"de": "Heimatadresse",
"en": "Home Address"
}
}
}
}
}
195 changes: 100 additions & 95 deletions apps/enmeshed/lib/account/my_data/file/file_detail_screen.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import 'dart:convert';

import 'package:enmeshed_runtime_bridge/enmeshed_runtime_bridge.dart';
import 'package:enmeshed_types/enmeshed_types.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/intl.dart';

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

class FileDetailScreen extends StatefulWidget {
final String accountId;
Expand All @@ -25,132 +30,118 @@ class FileDetailScreen extends StatefulWidget {
}

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

late FileDVO? _fileDVO;
late LocalAttributeDVO? _fileReferenceAttribute;
Siolto marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

late and ? is a VERY bad combination. Please explain here why you have chosen that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

late and ? is a VERY bad combination. Please explain here why you have chosen that.

You are right, in this case it is useless and causes confusion.

late List<String>? _tags;
late 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why always 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: EdgeInsets.only(top: 8, left: 24, right: 24, bottom: MediaQuery.viewInsetsOf(context).bottom + 42),
child: _fileDVO == null
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
padding: EdgeInsets.only(top: 16, left: 16, right: 16, bottom: MediaQuery.viewInsetsOf(context).bottom + 42),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
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),
),
],
),
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 (_fileReferenceAttribute != null) ...[
FileTagsContainer(tags: _tags?.first.split('+%+'), tagCollection: _tagCollection, onEditFile: _onEditFilePressed),
Gaps.h16,
],
FileInfoContainer(createdBy: _fileDVO!.createdBy.name, createdAt: _fileDVO!.createdAt),
Gaps.h32,
Row(
children: [
if (_fileReferenceAttribute != null) IconButton(onPressed: _onEditFilePressed, icon: const Icon(Icons.edit_outlined, size: 24)),
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),
),
],
),
],
),
),
),
);
}

String _formatDate(BuildContext context, String date) {
final locale = Localizations.localeOf(context);
final parsedDate = DateTime.parse(date).toLocal();
return DateFormat('EEEE, d. MMMM y', locale.toString()).format(parsedDate);
Future<void> _load() async {
await _loadFile();
await _loadTagCollection();
await _loadTags();
}

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);
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 {
// TODO(aince42): this is a temporary solution to load the tag collection
final jsonString = await rootBundle.loadString('assets/tag_example.json');
final jsonData = json.decode(jsonString) as Map<String, dynamic>;
final tagCollection = AttributeTagCollectionDTO.fromJson(jsonData);
// final tagCollection = await _session.consumptionServices.attributes.getAttributeTagCollection();

setState(() => _tagCollection = tagCollection);
}

Future<void> _loadTags({String? attributeId}) async {
if (_fileReferenceAttribute != null) {
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);

Expand Down Expand Up @@ -180,4 +171,18 @@ class _FileDetailScreenState extends State<FileDetailScreen> {

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

void _onEditFilePressed() {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (_) => EditFile(
accountId: widget.accountId,
fileTitle: _fileDVO!.title,
fileReferenceAttribute: _fileReferenceAttribute!,
tagCollection: _tagCollection,
onSave: _loadTags,
),
);
}
}
6 changes: 4 additions & 2 deletions apps/enmeshed/lib/account/my_data/file/files_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class _FilesScreenState extends State<FilesScreen> {
accountId: widget.accountId,
fileRecord: _filteredFileRecords[index],
trailing: const Icon(Icons.chevron_right),
reload: _loadFiles,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need the reload again?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need the reload again?

So that the FilesScreen is also updated as soon as an update has been made to the file.
If you navigate back and then click on the file again, the “fileReferenceAttribute” is passed directly from the FilesScreen.
If this is not up-to-date, the current tags are not displayed in the FileDetailScreen either.

),
itemCount: _filteredFileRecords.length,
separatorBuilder: (context, index) => const Divider(height: 2, indent: 16),
Expand Down Expand Up @@ -255,13 +256,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
Loading