simai_flutter 0.4.0
simai_flutter: ^0.4.0 copied to clipboard
A Flutter package for parsing, converting, and rendering simai format charts. Supports chart visualization with Flame engine and audio synchronization.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:simai_flutter/simai_flutter.dart';
void main() {
runApp(const MaterialApp(home: ExampleApp()));
}
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
SimaiPlayerController? _controller;
SimaiGameplayController? _gameplayController;
String? _audioAsset;
ImageProvider? _bgImageProvider;
SimaiVideoMetadata? _videoMetadata;
double _chartOffset = 0.0;
Key _playerKey = UniqueKey();
@override
void initState() {
super.initState();
_loadChart();
}
Future<void> _loadChart() async {
// Load from assets (works on Web, Mobile, Desktop)
const assetPath = 'assets/Never Give Up!/maidata.txt';
try {
final content = await rootBundle.loadString(assetPath);
var simaiFile = SimaiFile(content);
// Try loading Master (4) first, then others
// Priorities: 4 (Master), 3 (Expert), 5 (Re:Master), 2 (Advanced), 1 (Basic)
var chartText = simaiFile.getValue("inote_6");
var firstStr = simaiFile.getValue("first");
double offset = 0.0;
if (firstStr != null) {
offset = double.tryParse(firstStr) ?? 0.0;
}
if (chartText != null) {
final chart = SimaiConvert.deserialize(chartText);
setState(() {
_chartOffset = offset;
_audioAsset = 'assets/Never Give Up!/track.mp3';
_bgImageProvider = const AssetImage('assets/Never Give Up!/bg.png');
_controller?.dispose();
_gameplayController?.dispose();
_controller = SimaiPlayerController(
chart: chart,
audioAsset: _audioAsset,
backgroundImageProvider: _bgImageProvider,
initialChartTime: -_chartOffset,
)..title = "Never Give Up!";
_gameplayController = SimaiGameplayController(
chart: chart,
audioAsset: _audioAsset,
backgroundImageProvider: _bgImageProvider,
initialChartTime: -_chartOffset,
title: 'Never Give Up!',
);
_videoMetadata = SimaiVideoMetadata.fromSimaiFile(
simaiFile,
coverImageProvider: _bgImageProvider!,
difficulty: SimaiChartDifficulty.reMaster,
);
_playerKey = UniqueKey();
});
return;
}
} catch (e) {
debugPrint("Error loading chart from assets: $e");
}
// Fallback to sample chart if file load fails
const sampleChart = """
&inote_1=(140){4}
1,2,3,4,5,6,7,8,
1h[4:1],2h[4:1],3h[4:1],4h[4:1],
1b,2b,3b,4b,
C,A1,A2,A3,A4,A5,A6,A7,A8,
B1,B2,B3,B4,B5,B6,B7,B8,
1-4[4:1],2-5[4:1],
E
""";
var simaiFile = SimaiFile(sampleChart);
var chartText = simaiFile.getValue("inote_1");
if (chartText != null) {
final chart = SimaiConvert.deserialize(chartText);
setState(() {
_chartOffset = 0.0;
_audioAsset = null;
_bgImageProvider = null;
_controller?.dispose();
_gameplayController?.dispose();
_controller = SimaiPlayerController(
chart: chart,
audioAsset: _audioAsset,
backgroundImageProvider: _bgImageProvider,
initialChartTime: -_chartOffset,
);
_gameplayController = SimaiGameplayController(
chart: chart,
audioAsset: _audioAsset,
backgroundImageProvider: _bgImageProvider,
initialChartTime: -_chartOffset,
title: 'Sample Chart',
);
_videoMetadata = null;
_playerKey = UniqueKey();
});
}
}
@override
void dispose() {
_controller?.dispose();
_gameplayController?.dispose();
super.dispose();
}
void _openPlayer() {
final controller = _controller;
if (controller == null) return;
Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (context) => SimaiPlayerPage(
key: _playerKey,
controller: controller,
gameplayController: _gameplayController,
disposeController: false,
videoExportMetadata: _videoMetadata,
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(title: const Text('simai_flutter example')),
body: _controller == null || _gameplayController == null
? const Center(child: CircularProgressIndicator())
: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton.icon(
onPressed: _openPlayer,
icon: const Icon(Icons.play_circle_outline),
label: const Padding(
padding: EdgeInsets.symmetric(vertical: 14),
child: Text('打开谱面预览'),
),
),
],
),
),
),
),
);
}
}