Flutter标签管理插件flutter_taggy的使用
Flutter标签管理插件flutter_taggy的使用
目录
功能
- 📖 从文件读取音频标签元数据。
- 📝 写入音频标签。
- ✂ 删除音频标签。
- 🎶 支持多种文件格式: MP3, MP4, FLAC等。
计划功能 ⏳
- 批量处理:同时写入多个文件。
- 编辑文件名:根据轨道标题重命名文件。
- TaggyFileResult:所有公共API应返回通用结果;
一个
TaggyFile
类型的值或一个TaggyError
类型的错误。 - 转换标签
开始使用
安装
运行以下命令:
flutter pub add flutter_taggy
用法
初始化
import 'package:flutter_taggy/flutter_taggy.dart';
void main(){
// 添加此行
Taggy.initialize();
runApp(MyApp());
}
关于TaggyFile
-
它给我们提供了关于我们要读取或写入的文件的一些额外信息,因此除了
Tag
列表外,我们还得到了:- 文件大小(字节)。
FileType
:是否为(flac, wav, mpeg等)。AudioInfo
,另一种类型,包含音频轨道的属性。
-
您可以通过调用
formatAsAString()
来格式化一个TaggyFile
实例:输出示例
TaggyFile: { size: 12494053 bytes ~ 12.2 MB, fileType: FileType.Mpeg primaryTagType: TagType.Id3v2, tags: { count: 1, items: [ Tag( tagType: Id3v2, trackTitle: Fine Line, trackArtist: Eminem, trackNumber: 9, trackTotal: 1, discTotal: null, discNumber: null, album: SHADYXV, albumArtist: Various Artists, genre: null, language: null, year: null, recordingDate: null, originalReleaseDate: null, has lyrics: true, pictures: { count: 1, items: [ Picture( picType: PictureType.CoverFront, picData(Bytes): 168312, mimeType: MimeType.Jpeg, width: 1000, height: 1000, colorDepth: 24, numColors: 0, )], }, ), ], }, audio: AudioInfo( channelMask: 3, channels: 2, sampleRate: 44100, audioBitrate: 321, overallBitrate: 326, bitDepth: null, durationSec: 306, ), }
读取标签
-
读取所有标签:
```dart const path = 'path/to/audio/file.mp3';final TaggyFile taggyFile = await Taggy.readAll(path); // 您可以使用getter,底层是[taggyFile.tags.firstOrNull] print(taggyFile.firstTagIfAny);
// 或轻松访问所有返回的标签 for (var tag in taggyFile.tags) { print(tag.tagType); }
</li> <li> <p><strong>读取主标签:</strong></p> ```dart final path = 'path/to/audio/file.mp3'; final TaggyFile taggyFile = await Taggy.readPrimary(path);
-
读取任意标签:
它类似于
```dart const path = 'path/to/audio/file.mp3'; final TaggyFile taggyFile = await Taggy.readAny(path);readPrimary
,但返回的TaggyFile.tags
可能为空。// 您也可以使用[formatAsString],我们仍然得到一个[TaggyFile]。 print(taggyFile.formatAsString());
// 您可能想检查是否有任何标签 final hasTags = taggyFile.tags.isNotEmpty(); // 或使用getter final Tag? tag = taggyFile.firstTagIfAny;
</li> </ul> ### 写入标签 <ul> <li> <p><strong>关于指定<code>TagType</code></strong></p> <p>创建新<code>Tag</code>实例时需要一个标签类型。您可以通过以下方式指定:</p> <ul> <li> <p>检查基于其类型(扩展名)支持的<code>TagType</code>。参见此<a href="https://github.com/Serial-ATA/lofty-rs/blob/main/SUPPORTED_FORMATS.md">表</a>。</p> </li> <li> <p>使用函数<code>Taggy.writePrimary()</code> 并传递一个<code>Tag</code>,其类型为<code>TagType.FilePrimaryType</code>,如下面的示例所示。</p> </li> </ul> </li> <li> <details> <summary>创建新标签的示例</summary> ```dart Tag getTagInstance(TagType tagType){ return Tag( tagType: tagType, album: 'Some Album', trackTitle: 'some Track', trackArtist: 'Some Artist', trackTotal: 10, trackNumber: 1, discNumber: 1, discTotal: 2, year: 2023, recordingDate: '1/3/2019', language: 'EN', pictures: [ Picture( // 零用于演示如何提供图片的数据。 picData: Uint8List.fromList([0, 0, 0, 0]), mimeType: MimeType.Jpeg, picType: PictureType.CoverFront, width: 1000, height: 800, ), ], ); }
-
写入主标签:
```dart final path = 'path/to/audio/file.mp3';final tagToWrite = getTagInstance(TagType.FilePrimaryType);
final TaggyFile taggyFile = await Taggy.writePrimary( path: path, tag: tagToWrite, keepOthers: false);
// 成功后,[taggyFile.tags]将包含新添加的标签。 // 注意:此标签可能不包含与[tagToWrite]相同的属性。 final pTag = taggyFile.primaryTag;
</li> <li> <p><strong>写入多个标签</strong>:</p> <p>在大多数情况下,您会使用<code>Taggy.writePrimary()</code>来添加或编辑音频标签元数据, 但您也可以提供多个标签写入同一文件。</p> ```dart final path = 'path/to/audio/file.mp3'; final tags = [ getTagInstance(TagType.FilePrimaryType), getTagInstance(TagType.Id3v1), ]; final TaggyFile taggyFile = await Taggy.writeAll( path: path, tags: tags, overrideExistent: true);
删除标签
-
删除特定标签
通过指定标签类型,可以从文件中删除标签。
```dart final path = 'path/to/audio/file.mp3';// 要删除的标签类型 final tagType = TagType.Ape;
final TaggyFile taggyFile = await Taggy.removeTag(path: path, tagType: tagType);
</li> <li> <h4>删除所有标签</h4> ```dart final path = 'path/to/audio/file.mp3'; final TaggyFile taggyFile = await Taggy.removeAll(path: path); print(taggyFile.tags); // 输出为空[]
反馈与贡献
致谢
-
lofty:一个Rust库,为
Taggy
提供功能。 -
Flutter Rust Bridge:连接
Rust
API与Dart和Flutter。
示例代码
import 'dart:math';
import 'package:example/utils.dart';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:flutter/material.dart';
import 'package:flutter_taggy/flutter_taggy.dart';
/// Taggy的主题颜色
const kTaggyColor = Color(0xFF44A5DD);
extension BuildContextExtension on BuildContext {
ColorScheme get colorScheme => Theme.of(this).colorScheme;
double get screenWidth => MediaQuery.of(this).size.width;
}
void main() {
// 在使用[flutter_taggy]之前记得调用[initialize]
Taggy.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// 这个小部件是你的应用程序的根。
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: "flutter_taggy Demo",
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: kTaggyColor),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
[@override](/user/override)
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isPickingAFile = false;
String? pickedFilePath;
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: context.colorScheme.primaryContainer,
title: const Text("Taggy Demo"),
),
body: Center(
child: ListView(
children: [
FileSelectionSection(
onSelectFile: onSelectFile, pickedFilePath: pickedFilePath),
if (pickedFilePath != null) ...[
const ListTile(
title: Text(
'第二步:',
style: TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
'从功能区选择您想要探索的操作:',
),
),
ReadTagsSection(filePath: pickedFilePath!),
const SizedBox(height: 10),
WritingTagsSection(filePath: pickedFilePath!),
const SizedBox(height: 10),
RemoveTagsSection(filePath: pickedFilePath!),
],
],
),
),
);
}
Future<void> onSelectFile() async {
if (isPickingAFile) {
return;
} else {
isPickingAFile = true;
final pickedFile = await fp.FilePicker.platform.pickFiles(
allowCompression: false,
type: fp.FileType.custom,
lockParentWindow: true,
allowedExtensions: [
'FLAC',
'Flac',
'aac',
'mp3',
'mp4',
'wav',
],
);
isPickingAFile = false;
// 我们只需要已选文件的路径
setState(() {
pickedFilePath = pickedFile?.paths.first;
});
}
}
}
class FileSelectionSection extends StatelessWidget {
const FileSelectionSection({
super.key,
required this.onSelectFile,
required this.pickedFilePath,
});
final void Function() onSelectFile;
final String? pickedFilePath;
[@override](/user/override)
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
const ListTile(
title: Text('第一步:', style: TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(
'您需要指定要执行操作的文件路径。',
),
),
Expanded(
flex: 0,
child: Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.center,
children: [
const Text(
'已选文件路径: ',
style:
TextStyle(fontWeight: FontWeight.w500, color: kTaggyColor),
),
Text('"${pickedFilePath ?? 'None'}"'),
SizedBox(width: MediaQuery.of(context).size.width * .3),
TextButton.icon(
onPressed: onSelectFile,
label: const Text("选择音频文件"),
icon: const Icon(Icons.file_open_outlined),
),
],
),
),
const Divider(),
],
);
}
}
class ReadTagsSection extends StatelessWidget {
const ReadTagsSection({super.key, required this.filePath});
final String filePath;
[@override](/user/override)
Widget build(BuildContext context) {
return SectionCard(
title: '读取标签',
iconData: Icons.sticky_note_2_outlined,
content: ButtonBar(
alignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () =>
handleTaggyMethodCall(context, Taggy.readAll(filePath)),
child: const Text('读取所有'),
),
FilledButton.tonal(
onPressed: () =>
handleTaggyMethodCall(context, Taggy.readPrimary(filePath)),
child: const Text('读取主标签'),
),
FilledButton.tonal(
onPressed: () =>
handleTaggyMethodCall(context, Taggy.readAny(filePath)),
child: const Text('读取任意'),
),
],
),
);
}
}
class WritingTagsSection extends StatefulWidget {
const WritingTagsSection({super.key, required this.filePath});
final String filePath;
[@override](/user/override)
State<WritingTagsSection> createState() => _WritingTagsSectionState();
}
class _WritingTagsSectionState extends State<WritingTagsSection> {
double tagsToWriteCount = 1;
List<Tag> tags = [];
Tag tag = const Tag(tagType: TagType.FilePrimaryType, pictures: []);
bool keepOtherTags = false;
[@override](/user/override)
Widget build(BuildContext context) {
return SectionCard(
title: '写入标签',
iconData: Icons.edit_outlined,
content: Padding(
padding: const EdgeInsets.all(12.0),
child: SizedBox(
width: context.screenWidth * .8,
child: Wrap(
alignment: WrapAlignment.spaceBetween,
spacing: 10,
runSpacing: 12,
children: [
Text(
'作为主标签写入:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: context.colorScheme.onPrimaryContainer,
),
),
FilledButton.tonal(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return Dialog(
child: Container(
width: min(context.screenWidth * .8, 500),
padding: const EdgeInsets.symmetric(
vertical: 16, horizontal: 18),
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
'标签属性',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
FilledButton(
onPressed: () =>
Navigator.of(context).pop(),
child: const Text('完成'),
),
],
),
Expanded(
child: ListView(
shrinkWrap: true,
padding:
const EdgeInsets.symmetric(vertical: 30),
children: [
TagPropTextField(
label: '曲目标题',
initialValue: tag.trackTitle,
onChanged: (value) {
tag = tag.copyWith(trackTitle: value);
},
),
TagPropTextField(
label: '曲目艺术家',
initialValue: tag.trackArtist,
onChanged: (value) {
tag = tag.copyWith(trackArtist: value);
},
),
TagPropTextField(
label: '曲目专辑',
initialValue: tag.album,
onChanged: (value) {
tag = tag.copyWith(album: value);
},
),
TagPropTextField(
label: '专辑艺术家',
initialValue: tag.albumArtist,
onChanged: (value) {
tag = tag.copyWith(albumArtist: value);
},
),
TagPropTextField(
label: '制作人',
initialValue: tag.producer,
onChanged: (value) {
tag = tag.copyWith(producer: value);
},
),
TagPropTextField(
label: '发行日期',
initialValue: tag.originalReleaseDate,
onChanged: (value) {
tag = tag.copyWith(
originalReleaseDate: value);
},
),
TagPropTextField(
label: '录制日期',
initialValue: tag.recordingDate,
onChanged: (value) {
tag =
tag.copyWith(recordingDate: value);
},
),
TagPropTextField(
label: '年份',
initialValue: tag.year?.toString(),
onChanged: (value) {
if (value != null) {
tag = tag.copyWith(
year: int.tryParse(value));
} else {
tag = tag.copyWith(year: null);
}
},
),
TagPropTextField(
label: '曲目编号',
initialValue: tag.trackNumber?.toString(),
onChanged: (value) {
if (value != null) {
tag = tag.copyWith(
trackNumber: int.tryParse(value));
} else {
tag = tag.copyWith(trackNumber: null);
}
},
),
TagPropTextField(
label: '总曲目数',
initialValue: tag.trackTotal?.toString(),
onChanged: (value) {
if (value != null) {
tag = tag.copyWith(
trackTotal: int.tryParse(value));
} else {
tag = tag.copyWith(trackTotal: null);
}
},
),
TagPropTextField(
label: '光盘编号',
initialValue: tag.discNumber?.toString(),
onChanged: (value) {
if (value != null) {
tag = tag.copyWith(
discNumber: int.tryParse(value));
} else {
tag = tag.copyWith(discNumber: null);
}
},
),
TagPropTextField(
label: '光盘总数',
initialValue: tag.discTotal?.toString(),
onChanged: (value) {
if (value != null) {
tag = tag.copyWith(
discTotal: int.tryParse(value));
} else {
tag = tag.copyWith(discTotal: null);
}
},
),
TagPropTextField(
label: '流派',
initialValue: tag.genre,
onChanged: (value) {
tag = tag.copyWith(genre: value);
},
),
TagPropTextField(
label: '语言',
initialValue: tag.language,
onChanged: (value) {
tag = tag.copyWith(language: value);
},
),
TagPropTextField(
label: '歌词',
initialValue: tag.lyrics,
onChanged: (value) {
tag = tag.copyWith(lyrics: value);
},
),
],
),
),
],
),
),
);
},
);
},
child: const Text('查看/编辑"要写入的标签"'),
),
SizedBox(
width: 230,
child: CheckboxListTile(
title: const Text(
'应保留其他标签: ',
style: TextStyle(fontSize: 12),
),
value: keepOtherTags,
onChanged: (value) {
setState(() => keepOtherTags = value ?? false);
},
),
),
FilledButton.tonal(
onPressed: () {
handleTaggyMethodCall(
context,
Taggy.writePrimary(
path: widget.filePath,
tag: tag,
keepOthers: keepOtherTags,
),
);
},
child: const Text('写入文件'),
),
],
),
),
),
);
}
}
class TagPropTextField extends StatelessWidget {
const TagPropTextField({
super.key,
this.initialValue,
required this.label,
required this.onChanged,
});
final String? initialValue;
final String label;
final void Function(String?) onChanged;
[@override](/user/override)
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: TextFormField(
onChanged: onChanged,
initialValue: initialValue,
decoration: InputDecoration(
label: Text(label),
border: OutlineInputBorder(
borderSide: BorderSide(color: context.colorScheme.secondary),
),
),
),
);
}
}
class RemoveTagsSection extends StatefulWidget {
const RemoveTagsSection({super.key, required this.filePath});
final String filePath;
[@override](/user/override)
State<RemoveTagsSection> createState() => _RemoveTagsSectionState();
}
class _RemoveTagsSectionState extends State<RemoveTagsSection> {
TagType tagToRemoveType = TagType.FilePrimaryType;
[@override](/user/override)
Widget build(BuildContext context) {
return SectionCard(
title: '删除标签',
iconData: Icons.sticky_note_2_outlined,
content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'删除所有标签:',
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
FilledButton.tonal(
onPressed: () =>
handleTaggyMethodCall(
context, Taggy.removeAll(widget.filePath)),
child: const Text('删除所有'),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
const Text(
'按标签类型删除:',
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
const Spacer(),
DropdownMenu(
onSelected: (type) {
if (type != null) {
setState(() => tagToRemoveType = type);
}
},
hintText: '选择标签类型',
dropdownMenuEntries: TagType.values
.map((e) => DropdownMenuEntry(
value: e,
label: e.name,
))
.toList(),
),
const SizedBox(width: 20),
FilledButton.tonal(
onPressed: () {
handleTaggyMethodCall(
context,
Taggy.removeTag(
path: widget.filePath, tagType: tagToRemoveType),
);
},
child: const Text("删除"),
),
],
),
),
],
),
),
);
}
}
class SectionCard extends StatelessWidget {
const SectionCard({
super.key,
required this.title,
required this.iconData,
required this.content,
});
final String title;
final IconData iconData;
final Widget content;
[@override](/user/override)
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: context.colorScheme.primaryContainer.withOpacity(.2),
borderRadius: BorderRadius.circular(14),
),
margin: const EdgeInsets.all(12),
child: Column(
children: [
ListTile(
leading: Icon(iconData, color: context.colorScheme.primary),
title: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w500,
color: context.colorScheme.onPrimaryContainer,
),
),
),
content,
],
),
);
}
}
更多关于Flutter标签管理插件flutter_taggy的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter标签管理插件flutter_taggy的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中使用flutter_taggy
插件来实现标签管理功能的一个简单示例。flutter_taggy
是一个用于创建和管理标签的Flutter包,非常适合在需要标签输入功能的应用中使用。
首先,确保你已经在pubspec.yaml
文件中添加了flutter_taggy
依赖:
dependencies:
flutter:
sdk: flutter
flutter_taggy: ^x.y.z # 请替换为最新版本号
然后,运行flutter pub get
来安装依赖。
接下来,在你的Dart文件中,你可以按照以下方式使用flutter_taggy
:
import 'package:flutter/material.dart';
import 'package:flutter_taggy/flutter_taggy.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Taggy Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TaggyDemoScreen(),
);
}
}
class TaggyDemoScreen extends StatefulWidget {
@override
_TaggyDemoScreenState createState() => _TaggyDemoScreenState();
}
class _TaggyDemoScreenState extends State<TaggyDemoScreen> {
final TextEditingController _controller = TextEditingController();
List<String> _tags = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Taggy Demo'),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TagInput(
controller: _controller,
suggestions: ['Flutter', 'Dart', 'React', 'Angular', 'Vue'],
initialSuggestions: ['Flutter', 'Dart'],
onDeleted: (tag) {
setState(() {
_tags.remove(tag);
});
},
onSubmit: (String tag) {
if (tag.isNotEmpty) {
setState(() {
_tags.add(tag);
_controller.clear();
});
}
},
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter a tag',
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
_controller.clear();
},
),
),
),
SizedBox(height: 16),
Text('Selected Tags:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Wrap(
spacing: 4,
runSpacing: 4,
children: List.generate(
_tags.length,
(index) => Chip(
label: Text(_tags[index]),
onDeleted: () {
setState(() {
_tags.removeAt(index);
});
},
),
),
),
],
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
代码解释:
- 依赖添加:在
pubspec.yaml
中添加flutter_taggy
依赖。 - 主应用:创建一个简单的Flutter应用,包含一个主屏幕
TaggyDemoScreen
。 - 状态管理:使用
StatefulWidget
来管理标签的添加和删除。 TagInput
组件:使用TagInput
组件来接收用户输入的标签。controller
:用于管理文本输入。suggestions
:提供自动完成的建议标签列表。initialSuggestions
:初始显示的建议标签列表。onDeleted
:当用户删除一个标签时的回调。onSubmit
:当用户提交一个新标签时的回调。decoration
:用于自定义输入框的外观。
- 显示已选标签:使用
Wrap
组件和Chip
组件来显示用户已经选择的标签,并允许用户删除这些标签。
这个示例展示了如何使用flutter_taggy
插件来创建一个简单的标签管理系统,包括标签的输入、自动完成、删除和显示。你可以根据自己的需求进一步自定义和扩展这个示例。