lain-the-slayer 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lain/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
lain/cli.py ADDED
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from importlib.resources import files as resource_files
6
+ from pathlib import Path
7
+
8
+ PRACTICALS = {
9
+ 1: ("Hello World & Flutter Setup", "01_hello_world.dart"),
10
+ 3: ("StatefulWidget Lifecycle", "03_stateful_lifecycle.dart"),
11
+ 4: ("Navigator.push() and Navigator.pop()", "04_navigator_navigation.dart"),
12
+ 6: ("Parent-Child State Management", "06_parent_child_state.dart"),
13
+ 9: ("Login Form & Validation", "09_login_form_validation.dart"),
14
+ 11: ("Create and Use a Menu", "11_popup_menu.dart"),
15
+ 13: ("DropdownButton - Dynamic Data", "13_dropdown_dynamic_data.dart"),
16
+ 16: ("GridView - Grid Layout", "16_gridview.dart"),
17
+ 17: ("ListView - Custom Widgets", "17_listview_custom_widgets.dart"),
18
+ 19: ("Expanded & Flexible", "19_expanded_flexible.dart"),
19
+ 20: ("Audio Media Playback", "20_audio_playback.dart"),
20
+ 21: ("GestureDetector & Button Callbacks", "21_gesture_detector.dart"),
21
+ }
22
+
23
+
24
+ def print_menu() -> None:
25
+ print("\n" + "=" * 56)
26
+ print(" LAIN - FLUTTER PRACTICALS")
27
+ print("=" * 56)
28
+ print("Select a practical:\n")
29
+ for number, (title, _) in PRACTICALS.items():
30
+ print(f" [{number:2}] {title}")
31
+ print("\n [ 0] Exit")
32
+ print("=" * 56)
33
+
34
+
35
+ def get_source(filename: str) -> str:
36
+ return (
37
+ resource_files("lain")
38
+ .joinpath("practicals")
39
+ .joinpath(filename)
40
+ .read_text(encoding="utf-8")
41
+ )
42
+
43
+
44
+ def write_source(number: int, output: Path, flutter: bool) -> Path:
45
+ title, filename = PRACTICALS[number]
46
+ destination = output / "lib" / "main.dart" if flutter else output / f"practical_{number:02}.dart"
47
+
48
+ if flutter:
49
+ if not (output / "pubspec.yaml").is_file() or not (output / "lib").is_dir():
50
+ raise ValueError("--flutter requires the current directory to be a Flutter project.")
51
+ if destination.exists():
52
+ answer = input("lib/main.dart already exists. Overwrite it? [y/N]: ").strip().lower()
53
+ if answer not in {"y", "yes"}:
54
+ print("Cancelled; existing file was not changed.")
55
+ return destination
56
+
57
+ destination.parent.mkdir(parents=True, exist_ok=True)
58
+ destination.write_text(get_source(filename), encoding="utf-8")
59
+ print(f"\n[+] Practical {number} selected: {title}")
60
+ print("[+] Dart source generated successfully.")
61
+ print(f"[+] Location: {destination.resolve()}")
62
+
63
+ if number == 20:
64
+ print("\n[!] Practical 20 requires setup:")
65
+ print(" Add audioplayers to pubspec.yaml, add assets/audio/music.mp3,")
66
+ print(" register that asset, then run: flutter pub get")
67
+ return destination
68
+
69
+
70
+ def main() -> None:
71
+ parser = argparse.ArgumentParser(
72
+ prog="lain",
73
+ description="Generate Dart source files for the 11 highlighted Flutter practicals.",
74
+ )
75
+ parser.add_argument("--list", action="store_true", help="List all available practicals.")
76
+ parser.add_argument("--show", type=int, metavar="NUMBER", help="Print a practical's Dart source.")
77
+ parser.add_argument("--flutter", action="store_true", help="Write selected code to lib/main.dart in the current Flutter project.")
78
+ parser.add_argument("--output", type=Path, default=Path.cwd(), help="Output directory (default: current directory).")
79
+ args = parser.parse_args()
80
+
81
+ if args.list:
82
+ print_menu()
83
+ return
84
+
85
+ if args.show is not None:
86
+ if args.show not in PRACTICALS:
87
+ parser.error(f"Practical {args.show} is not available.")
88
+ print(get_source(PRACTICALS[args.show][1]))
89
+ return
90
+
91
+ if args.flutter and args.output == Path.cwd():
92
+ # The current directory is intentionally used as the Flutter project root.
93
+ pass
94
+
95
+ while True:
96
+ print_menu()
97
+ raw = input("Enter practical number: ").strip()
98
+ if raw == "0":
99
+ print("Good luck with your practical exam!")
100
+ return
101
+ try:
102
+ number = int(raw)
103
+ except ValueError:
104
+ print("Please enter a valid practical number.")
105
+ continue
106
+ if number not in PRACTICALS:
107
+ print("That number is not in the exam list. Choose one of the listed numbers.")
108
+ continue
109
+ try:
110
+ write_source(number, args.output, args.flutter)
111
+ except (OSError, ValueError) as error:
112
+ print(f"[!] {error}")
113
+ return
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()
@@ -0,0 +1,25 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() {
4
+ runApp(const MyApp());
5
+ }
6
+
7
+ class MyApp extends StatelessWidget {
8
+ const MyApp({super.key});
9
+
10
+ @override
11
+ Widget build(BuildContext context) {
12
+ return const MaterialApp(
13
+ debugShowCheckedModeBanner: false,
14
+ home: Scaffold(
15
+ appBar: AppBar(title: Text('Hello World')),
16
+ body: Center(
17
+ child: Text(
18
+ 'Hello World',
19
+ style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
20
+ ),
21
+ ),
22
+ ),
23
+ );
24
+ }
25
+ }
@@ -0,0 +1,65 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: LifecycleDemo(),
6
+ ));
7
+
8
+ class LifecycleDemo extends StatefulWidget {
9
+ const LifecycleDemo({super.key});
10
+
11
+ @override
12
+ State<LifecycleDemo> createState() {
13
+ debugPrint('createState() called');
14
+ return _LifecycleDemoState();
15
+ }
16
+ }
17
+
18
+ class _LifecycleDemoState extends State<LifecycleDemo> {
19
+ int count = 0;
20
+
21
+ @override
22
+ void initState() {
23
+ super.initState();
24
+ debugPrint('initState() called');
25
+ }
26
+
27
+ @override
28
+ void didChangeDependencies() {
29
+ super.didChangeDependencies();
30
+ debugPrint('didChangeDependencies() called');
31
+ }
32
+
33
+ @override
34
+ Widget build(BuildContext context) {
35
+ debugPrint('build() called');
36
+ return Scaffold(
37
+ appBar: AppBar(title: const Text('StatefulWidget Lifecycle')),
38
+ body: Center(
39
+ child: Column(
40
+ mainAxisAlignment: MainAxisAlignment.center,
41
+ children: [
42
+ Text('Counter: $count', style: const TextStyle(fontSize: 28)),
43
+ const SizedBox(height: 16),
44
+ ElevatedButton(
45
+ onPressed: () => setState(() => count++),
46
+ child: const Text('Increment'),
47
+ ),
48
+ ],
49
+ ),
50
+ ),
51
+ );
52
+ }
53
+
54
+ @override
55
+ void deactivate() {
56
+ debugPrint('deactivate() called');
57
+ super.deactivate();
58
+ }
59
+
60
+ @override
61
+ void dispose() {
62
+ debugPrint('dispose() called');
63
+ super.dispose();
64
+ }
65
+ }
@@ -0,0 +1,60 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() {
4
+ runApp(const MyApp());
5
+ }
6
+
7
+ class MyApp extends StatelessWidget {
8
+ const MyApp({super.key});
9
+
10
+ @override
11
+ Widget build(BuildContext context) {
12
+ return const MaterialApp(
13
+ debugShowCheckedModeBanner: false,
14
+ home: FirstScreen(),
15
+ );
16
+ }
17
+ }
18
+
19
+ class FirstScreen extends StatelessWidget {
20
+ const FirstScreen({super.key});
21
+
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ return Scaffold(
25
+ appBar: AppBar(title: const Text('First Screen')),
26
+ body: Center(
27
+ child: ElevatedButton(
28
+ child: const Text('Go to Next Screen'),
29
+ onPressed: () {
30
+ Navigator.push(
31
+ context,
32
+ MaterialPageRoute(
33
+ builder: (context) => const SecondScreen(),
34
+ ),
35
+ );
36
+ },
37
+ ),
38
+ ),
39
+ );
40
+ }
41
+ }
42
+
43
+ class SecondScreen extends StatelessWidget {
44
+ const SecondScreen({super.key});
45
+
46
+ @override
47
+ Widget build(BuildContext context) {
48
+ return Scaffold(
49
+ appBar: AppBar(title: const Text('Second Screen')),
50
+ body: Center(
51
+ child: ElevatedButton(
52
+ child: const Text('Back to First Screen'),
53
+ onPressed: () {
54
+ Navigator.pop(context);
55
+ },
56
+ ),
57
+ ),
58
+ );
59
+ }
60
+ }
@@ -0,0 +1,54 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: ParentScreen(),
6
+ ));
7
+
8
+ class ParentScreen extends StatefulWidget {
9
+ const ParentScreen({super.key});
10
+
11
+ @override
12
+ State<ParentScreen> createState() => _ParentScreenState();
13
+ }
14
+
15
+ class _ParentScreenState extends State<ParentScreen> {
16
+ int count = 0;
17
+
18
+ void increase() {
19
+ setState(() => count++);
20
+ }
21
+
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ return Scaffold(
25
+ appBar: AppBar(title: const Text('Parent Child Example')),
26
+ body: Center(
27
+ child: ChildScreen(number: count, onPressed: increase),
28
+ ),
29
+ );
30
+ }
31
+ }
32
+
33
+ class ChildScreen extends StatelessWidget {
34
+ final int number;
35
+ final VoidCallback onPressed;
36
+
37
+ const ChildScreen({
38
+ super.key,
39
+ required this.number,
40
+ required this.onPressed,
41
+ });
42
+
43
+ @override
44
+ Widget build(BuildContext context) {
45
+ return Column(
46
+ mainAxisAlignment: MainAxisAlignment.center,
47
+ children: [
48
+ Text('Count = $number', style: const TextStyle(fontSize: 25)),
49
+ const SizedBox(height: 20),
50
+ ElevatedButton(onPressed: onPressed, child: const Text('Increase')),
51
+ ],
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,73 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: LoginPage(),
6
+ ));
7
+
8
+ class LoginPage extends StatefulWidget {
9
+ const LoginPage({super.key});
10
+
11
+ @override
12
+ State<LoginPage> createState() => _LoginPageState();
13
+ }
14
+
15
+ class _LoginPageState extends State<LoginPage> {
16
+ final formKey = GlobalKey<FormState>();
17
+
18
+ @override
19
+ Widget build(BuildContext context) {
20
+ return Scaffold(
21
+ appBar: AppBar(title: const Text('Login Form')),
22
+ body: Padding(
23
+ padding: const EdgeInsets.all(20),
24
+ child: Form(
25
+ key: formKey,
26
+ child: Column(
27
+ children: [
28
+ TextFormField(
29
+ decoration: const InputDecoration(
30
+ labelText: 'Email',
31
+ border: OutlineInputBorder(),
32
+ ),
33
+ validator: (value) {
34
+ if (value == null || value.isEmpty) {
35
+ return 'Enter Email';
36
+ }
37
+ if (!value.contains('@')) return 'Enter a valid email';
38
+ return null;
39
+ },
40
+ ),
41
+ const SizedBox(height: 15),
42
+ TextFormField(
43
+ obscureText: true,
44
+ decoration: const InputDecoration(
45
+ labelText: 'Password',
46
+ border: OutlineInputBorder(),
47
+ ),
48
+ validator: (value) {
49
+ if (value == null || value.isEmpty) {
50
+ return 'Enter Password';
51
+ }
52
+ if (value.length < 6) return 'Minimum 6 characters';
53
+ return null;
54
+ },
55
+ ),
56
+ const SizedBox(height: 15),
57
+ ElevatedButton(
58
+ onPressed: () {
59
+ if (formKey.currentState!.validate()) {
60
+ ScaffoldMessenger.of(context).showSnackBar(
61
+ const SnackBar(content: Text('Login Successful')),
62
+ );
63
+ }
64
+ },
65
+ child: const Text('LOGIN'),
66
+ ),
67
+ ],
68
+ ),
69
+ ),
70
+ ),
71
+ );
72
+ }
73
+ }
@@ -0,0 +1,40 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: MenuDemo(),
6
+ ));
7
+
8
+ class MenuDemo extends StatefulWidget {
9
+ const MenuDemo({super.key});
10
+
11
+ @override
12
+ State<MenuDemo> createState() => _MenuDemoState();
13
+ }
14
+
15
+ class _MenuDemoState extends State<MenuDemo> {
16
+ String selected = 'Nothing selected';
17
+
18
+ @override
19
+ Widget build(BuildContext context) {
20
+ return Scaffold(
21
+ appBar: AppBar(
22
+ title: const Text('Menu Example'),
23
+ actions: [
24
+ PopupMenuButton<String>(
25
+ onSelected: (value) => setState(() => selected = value),
26
+ itemBuilder: (context) => const [
27
+ PopupMenuItem(value: 'Home', child: Text('Home')),
28
+ PopupMenuItem(value: 'About', child: Text('About')),
29
+ PopupMenuItem(value: 'Exit', child: Text('Exit')),
30
+ ],
31
+ ),
32
+ ],
33
+ ),
34
+ body: Center(
35
+ child: Text('Selected: $selected',
36
+ style: const TextStyle(fontSize: 24)),
37
+ ),
38
+ );
39
+ }
40
+ }
@@ -0,0 +1,47 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: DropdownDemo(),
6
+ ));
7
+
8
+ class DropdownDemo extends StatefulWidget {
9
+ const DropdownDemo({super.key});
10
+
11
+ @override
12
+ State<DropdownDemo> createState() => _DropdownDemoState();
13
+ }
14
+
15
+ class _DropdownDemoState extends State<DropdownDemo> {
16
+ final List<String> items = ['Apple', 'Banana', 'Mango', 'Orange'];
17
+ String selected = 'Apple';
18
+
19
+ @override
20
+ Widget build(BuildContext context) {
21
+ return Scaffold(
22
+ appBar: AppBar(title: const Text('Dropdown Example')),
23
+ body: Center(
24
+ child: Column(
25
+ mainAxisAlignment: MainAxisAlignment.center,
26
+ children: [
27
+ DropdownButton<String>(
28
+ value: selected,
29
+ items: items.map((item) {
30
+ return DropdownMenuItem<String>(
31
+ value: item,
32
+ child: Text(item),
33
+ );
34
+ }).toList(),
35
+ onChanged: (value) {
36
+ if (value != null) setState(() => selected = value);
37
+ },
38
+ ),
39
+ const SizedBox(height: 20),
40
+ Text('Selected: $selected',
41
+ style: const TextStyle(fontSize: 20)),
42
+ ],
43
+ ),
44
+ ),
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,44 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: GridDemo(),
6
+ ));
7
+
8
+ class GridDemo extends StatelessWidget {
9
+ const GridDemo({super.key});
10
+
11
+ final List<String> items = const [
12
+ 'Apple', 'Banana', 'Mango', 'Orange', 'Grapes', 'Pineapple'
13
+ ];
14
+
15
+ @override
16
+ Widget build(BuildContext context) {
17
+ return Scaffold(
18
+ appBar: AppBar(title: const Text('GridView Example')),
19
+ body: Padding(
20
+ padding: const EdgeInsets.all(20),
21
+ child: GridView.builder(
22
+ itemCount: items.length,
23
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
24
+ crossAxisCount: 2,
25
+ crossAxisSpacing: 10,
26
+ mainAxisSpacing: 10,
27
+ childAspectRatio: 2.5,
28
+ ),
29
+ itemBuilder: (context, index) {
30
+ return Container(
31
+ color: Colors.blue,
32
+ child: Center(
33
+ child: Text(
34
+ items[index],
35
+ style: const TextStyle(color: Colors.white, fontSize: 18),
36
+ ),
37
+ ),
38
+ );
39
+ },
40
+ ),
41
+ ),
42
+ );
43
+ }
44
+ }
@@ -0,0 +1,47 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: StudentListPage(),
6
+ ));
7
+
8
+ class StudentListPage extends StatelessWidget {
9
+ const StudentListPage({super.key});
10
+
11
+ final List<String> students = const [
12
+ 'Rahul', 'Priya', 'Amit', 'Sneha', 'Arjun'
13
+ ];
14
+
15
+ @override
16
+ Widget build(BuildContext context) {
17
+ return Scaffold(
18
+ appBar: AppBar(title: const Text('Student List')),
19
+ body: ListView.builder(
20
+ itemCount: students.length,
21
+ itemBuilder: (context, index) {
22
+ return StudentTile(name: students[index], number: index + 1);
23
+ },
24
+ ),
25
+ );
26
+ }
27
+ }
28
+
29
+ class StudentTile extends StatelessWidget {
30
+ final String name;
31
+ final int number;
32
+
33
+ const StudentTile({super.key, required this.name, required this.number});
34
+
35
+ @override
36
+ Widget build(BuildContext context) {
37
+ return Card(
38
+ margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
39
+ child: ListTile(
40
+ leading: CircleAvatar(child: Text('$number')),
41
+ title: Text(name),
42
+ subtitle: const Text('Computer Science Student'),
43
+ trailing: const Icon(Icons.arrow_forward_ios),
44
+ ),
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,74 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: ResponsiveDemo(),
6
+ ));
7
+
8
+ class ResponsiveDemo extends StatelessWidget {
9
+ const ResponsiveDemo({super.key});
10
+
11
+ @override
12
+ Widget build(BuildContext context) {
13
+ return Scaffold(
14
+ appBar: AppBar(title: const Text('Expanded and Flexible')),
15
+ body: Padding(
16
+ padding: const EdgeInsets.all(16),
17
+ child: Column(
18
+ children: [
19
+ const Text('Expanded widgets', style: TextStyle(fontSize: 20)),
20
+ const SizedBox(height: 10),
21
+ Row(
22
+ children: [
23
+ Expanded(
24
+ flex: 2,
25
+ child: Container(
26
+ height: 80,
27
+ color: Colors.blue,
28
+ child: const Center(
29
+ child: Text('Expanded 2',
30
+ style: TextStyle(color: Colors.white)),
31
+ ),
32
+ ),
33
+ ),
34
+ const SizedBox(width: 8),
35
+ Expanded(
36
+ flex: 1,
37
+ child: Container(
38
+ height: 80,
39
+ color: Colors.orange,
40
+ child: const Center(child: Text('Expanded 1')),
41
+ ),
42
+ ),
43
+ ],
44
+ ),
45
+ const SizedBox(height: 30),
46
+ const Text('Flexible widget', style: TextStyle(fontSize: 20)),
47
+ const SizedBox(height: 10),
48
+ Row(
49
+ children: [
50
+ Flexible(
51
+ child: Container(
52
+ height: 80,
53
+ color: Colors.green,
54
+ child: const Center(child: Text('Flexible')),
55
+ ),
56
+ ),
57
+ const SizedBox(width: 8),
58
+ Container(
59
+ width: 100,
60
+ height: 80,
61
+ color: Colors.purple,
62
+ child: const Center(
63
+ child: Text('Fixed width',
64
+ style: TextStyle(color: Colors.white)),
65
+ ),
66
+ ),
67
+ ],
68
+ ),
69
+ ],
70
+ ),
71
+ ),
72
+ );
73
+ }
74
+ }
@@ -0,0 +1,71 @@
1
+ // Practical 20: Audio playback using the audioplayers plugin.
2
+ //
3
+ // Before running:
4
+ // 1. Add audioplayers to pubspec.yaml:
5
+ // dependencies:
6
+ // flutter:
7
+ // sdk: flutter
8
+ // audioplayers: ^6.0.0
9
+ // 2. Put an audio file at assets/audio/music.mp3
10
+ // 3. Register it in pubspec.yaml:
11
+ // flutter:
12
+ // assets:
13
+ // - assets/audio/music.mp3
14
+ // 4. Run: flutter pub get
15
+
16
+ import 'package:flutter/material.dart';
17
+ import 'package:audioplayers/audioplayers.dart';
18
+
19
+ void main() => runApp(const MaterialApp(
20
+ debugShowCheckedModeBanner: false,
21
+ home: AudioDemo(),
22
+ ));
23
+
24
+ class AudioDemo extends StatefulWidget {
25
+ const AudioDemo({super.key});
26
+
27
+ @override
28
+ State<AudioDemo> createState() => _AudioDemoState();
29
+ }
30
+
31
+ class _AudioDemoState extends State<AudioDemo> {
32
+ final AudioPlayer player = AudioPlayer();
33
+ bool isPlaying = false;
34
+
35
+ Future<void> playAudio() async {
36
+ await player.play(AssetSource('audio/music.mp3'));
37
+ setState(() => isPlaying = true);
38
+ }
39
+
40
+ Future<void> pauseAudio() async {
41
+ await player.pause();
42
+ setState(() => isPlaying = false);
43
+ }
44
+
45
+ @override
46
+ void dispose() {
47
+ player.dispose();
48
+ super.dispose();
49
+ }
50
+
51
+ @override
52
+ Widget build(BuildContext context) {
53
+ return Scaffold(
54
+ appBar: AppBar(title: const Text('Audio Playback')),
55
+ body: Center(
56
+ child: Column(
57
+ mainAxisAlignment: MainAxisAlignment.center,
58
+ children: [
59
+ const Icon(Icons.music_note, size: 80),
60
+ const SizedBox(height: 20),
61
+ ElevatedButton.icon(
62
+ onPressed: isPlaying ? pauseAudio : playAudio,
63
+ icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
64
+ label: Text(isPlaying ? 'Pause Audio' : 'Play Audio'),
65
+ ),
66
+ ],
67
+ ),
68
+ ),
69
+ );
70
+ }
71
+ }
@@ -0,0 +1,60 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ void main() => runApp(const MaterialApp(
4
+ debugShowCheckedModeBanner: false,
5
+ home: GestureDemo(),
6
+ ));
7
+
8
+ class GestureDemo extends StatefulWidget {
9
+ const GestureDemo({super.key});
10
+
11
+ @override
12
+ State<GestureDemo> createState() => _GestureDemoState();
13
+ }
14
+
15
+ class _GestureDemoState extends State<GestureDemo> {
16
+ String message = 'Perform a gesture';
17
+
18
+ void updateMessage(String value) {
19
+ setState(() => message = value);
20
+ }
21
+
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ return Scaffold(
25
+ appBar: AppBar(title: const Text('Gesture Detector Demo')),
26
+ body: Center(
27
+ child: Column(
28
+ mainAxisAlignment: MainAxisAlignment.center,
29
+ children: [
30
+ GestureDetector(
31
+ onTap: () => updateMessage('Single Tap Detected'),
32
+ onDoubleTap: () => updateMessage('Double Tap Detected'),
33
+ onLongPress: () => updateMessage('Long Press Detected'),
34
+ child: Container(
35
+ padding: const EdgeInsets.all(30),
36
+ color: Colors.blue,
37
+ child: const Text(
38
+ 'Tap / Double Tap / Long Press',
39
+ style: TextStyle(color: Colors.white),
40
+ textAlign: TextAlign.center,
41
+ ),
42
+ ),
43
+ ),
44
+ const SizedBox(height: 20),
45
+ Text(message, style: const TextStyle(fontSize: 20)),
46
+ const SizedBox(height: 20),
47
+ ElevatedButton(
48
+ onPressed: () => updateMessage('ElevatedButton Pressed'),
49
+ child: const Text('Elevated Button'),
50
+ ),
51
+ TextButton(
52
+ onPressed: () => updateMessage('TextButton Pressed'),
53
+ child: const Text('Text Button'),
54
+ ),
55
+ ],
56
+ ),
57
+ ),
58
+ );
59
+ }
60
+ }
File without changes
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: lain-the-slayer
3
+ Version: 0.2.0
4
+ Summary: A CLI tool that generates Dart files for 11 highlighted Flutter practicals.
5
+ Author: Lain
6
+ License: MIT
7
+ Keywords: flutter,dart,practical,education,cli
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Environment :: Console
11
+ Classifier: Topic :: Education
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # Lain — Flutter Practicals CLI
18
+
19
+ Lain is a small Python command-line tool that generates Dart source files for the 11 highlighted Flutter practicals.
20
+
21
+ ## Included practicals
22
+
23
+ 1. Hello World & Flutter setup
24
+ 3. StatefulWidget lifecycle
25
+ 4. Navigator.push() and Navigator.pop()
26
+ 6. Parent-child state management
27
+ 9. Login form and validation
28
+ 11. Popup menu
29
+ 13. DropdownButton with dynamic data
30
+ 16. GridView
31
+ 17. ListView with custom widgets
32
+ 19. Expanded and Flexible
33
+ 20. Audio playback
34
+ 21. GestureDetector and button callbacks
35
+
36
+ ## Install locally
37
+
38
+ From this project directory:
39
+
40
+ ```bash
41
+ python -m pip install .
42
+ ```
43
+
44
+ Or install in editable/development mode:
45
+
46
+ ```bash
47
+ python -m pip install -e .
48
+ ```
49
+
50
+ ## Run
51
+
52
+ ```bash
53
+ lain
54
+ ```
55
+
56
+ Choose a practical number. Lain writes a Dart file into the current working directory, for example `practical_16.dart`.
57
+
58
+ ## Other commands
59
+
60
+ ```bash
61
+ lain --list
62
+ lain --show 16
63
+ lain --output ./generated
64
+ ```
65
+
66
+ To write the selected practical directly into an existing Flutter project's `lib/main.dart`, run `lain --flutter` from the Flutter project root. Lain checks for `pubspec.yaml` and asks before overwriting `lib/main.dart`.
67
+
68
+ ## Practical 20 setup
69
+
70
+ The audio practical uses the `audioplayers` Flutter plugin and an audio asset. Add this to the Flutter project's `pubspec.yaml`:
71
+
72
+ ```yaml
73
+ dependencies:
74
+ flutter:
75
+ sdk: flutter
76
+ audioplayers: ^6.0.0
77
+
78
+ flutter:
79
+ assets:
80
+ - assets/audio/music.mp3
81
+ ```
82
+
83
+ Place an audio file at `assets/audio/music.mp3`, then run:
84
+
85
+ ```bash
86
+ flutter pub get
87
+ ```
88
+
89
+ The generated Dart file is named `practical_20.dart`. For plugin-based practicals, additional Flutter dependencies and assets may be required; see comments in the generated file.
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ python -m pip install -e .
95
+ lain --list
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,21 @@
1
+ lain/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
2
+ lain/cli.py,sha256=dk8hSWcKhg1USihVHIEkVvrfE2ln9atM7LDNPza5zsY,4471
3
+ lain/practicals/01_hello_world.dart,sha256=svu4jiI5bP7L027M6ss9dlqe5uFUTxZ50fi1hsd81D0,537
4
+ lain/practicals/03_stateful_lifecycle.dart,sha256=gToMY-9NwV6suQpDnIFAW3xKzrXRDN5jKYi2tykfOgU,1480
5
+ lain/practicals/04_navigator_navigation.dart,sha256=KQfubCDxG95PnNIFIrWRovIc7c7jQqHdQSGSC3sw-5I,1294
6
+ lain/practicals/06_parent_child_state.dart,sha256=VAQ_vhnbkKFMMfbJvJej8TWhpCe6c-hmN4lXNkiHG88,1228
7
+ lain/practicals/09_login_form_validation.dart,sha256=TcxmZfgvwhK2XEGtO8gEv1oeSx6tDXtmAXgysRYWVlQ,2203
8
+ lain/practicals/11_popup_menu.dart,sha256=8ySenhvsIjs7hNmlCcGRDRIWgMXx7AOg0rSCL6S0u9s,1071
9
+ lain/practicals/13_dropdown_dynamic_data.dart,sha256=bJ177p-YFYM31ksBZztx-GJhxgbcSbS6rlKM8MzBsd4,1310
10
+ lain/practicals/16_gridview.dart,sha256=YEY8uvaL7iDncrsh5pSZjv8QDlmjRRk2HiLnsEaTs4M,1190
11
+ lain/practicals/17_listview_custom_widgets.dart,sha256=hc1Ttjz_DzkryAGGwHAWeO3ojpTI0vXN_gh1VXg1YMs,1226
12
+ lain/practicals/19_expanded_flexible.dart,sha256=5i2lRPblvMc5b9duykiiJV8uvk0u4ph0apiCOM_XgTc,2253
13
+ lain/practicals/20_audio_playback.dart,sha256=PXSDltqT-VbrlbrAD3yzN4TCOvQmxw6-8Ke9aXgD4dk,1832
14
+ lain/practicals/21_gesture_detector.dart,sha256=aP4uo0itzokmgKthZIndceIa7uQt2R-IhXIS20of_O8,1851
15
+ lain/practicals/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ lain_the_slayer-0.2.0.dist-info/licenses/LICENSE,sha256=6WntgUGFd-PADyvZl4NrrnYdACTL2LrBPAd2PQHqF3A,810
17
+ lain_the_slayer-0.2.0.dist-info/METADATA,sha256=NI1QFSWTZQl_EHAJH1nO2DnWuo_5JYNt7UCKF22KXwE,2274
18
+ lain_the_slayer-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
19
+ lain_the_slayer-0.2.0.dist-info/entry_points.txt,sha256=tJdUUZk-NcSPeIAFwexjF6NDF55tsGNesrLUbKO_8dY,39
20
+ lain_the_slayer-0.2.0.dist-info/top_level.txt,sha256=bH3V4uoUY2hyNhrsMc2swOHscjBrkIMgLhWYKv0WZNM,5
21
+ lain_the_slayer-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lain = lain.cli:main
@@ -0,0 +1,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Owais Idrisi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -0,0 +1 @@
1
+ lain