flutter-dev 0.1.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.
- common_utils.py +261 -0
- core/__init__.py +7 -0
- core/constants.py +57 -0
- core/state.py +25 -0
- create_page.py +288 -0
- fdev.py +258 -0
- flutter_dev-0.1.0.dist-info/METADATA +411 -0
- flutter_dev-0.1.0.dist-info/RECORD +30 -0
- flutter_dev-0.1.0.dist-info/WHEEL +5 -0
- flutter_dev-0.1.0.dist-info/entry_points.txt +5 -0
- flutter_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
- flutter_dev-0.1.0.dist-info/top_level.txt +9 -0
- gemini_api.py +395 -0
- git_diff_output_editor.py +34 -0
- install_legacy.py +467 -0
- managers/__init__.py +69 -0
- managers/ai.py +113 -0
- managers/app.py +541 -0
- managers/brew.py +477 -0
- managers/build.py +436 -0
- managers/datetime.py +49 -0
- managers/device.py +207 -0
- managers/doctor.py +286 -0
- managers/git.py +981 -0
- managers/git_account.py +542 -0
- managers/merge.py +165 -0
- managers/mirror.py +205 -0
- managers/project.py +138 -0
- managers/web_deploy.py +43 -0
- switch_ai.py +181 -0
common_utils.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Common Utilities Module
|
|
4
|
+
Shared functions and constants for flutter-dev tools
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
import platform
|
|
11
|
+
import subprocess
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# COLOR CONSTANTS
|
|
17
|
+
# ============================================================================
|
|
18
|
+
|
|
19
|
+
# Colors for output
|
|
20
|
+
RED = '\033[0;31m'
|
|
21
|
+
GREEN = '\033[0;32m'
|
|
22
|
+
YELLOW = '\033[1;33m'
|
|
23
|
+
BLUE = '\033[0;34m'
|
|
24
|
+
NC = '\033[0m'
|
|
25
|
+
MAGENTA = '\033[0;35m'
|
|
26
|
+
CHECKMARK = '\033[32m✓\033[0m'
|
|
27
|
+
CROSS = '\033[31m✗\033[0m'
|
|
28
|
+
|
|
29
|
+
# Disable colors on Windows CMD (unless using Windows Terminal)
|
|
30
|
+
if platform.system() == "Windows" and not os.environ.get('WT_SESSION'):
|
|
31
|
+
RED = GREEN = YELLOW = BLUE = NC = MAGENTA = ''
|
|
32
|
+
CHECKMARK = '✓'
|
|
33
|
+
CROSS = '✗'
|
|
34
|
+
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# TIMER DECORATOR
|
|
37
|
+
# ============================================================================
|
|
38
|
+
|
|
39
|
+
def timer_decorator(func):
|
|
40
|
+
"""
|
|
41
|
+
Decorator to automatically add timer functionality to any function
|
|
42
|
+
"""
|
|
43
|
+
@wraps(func)
|
|
44
|
+
def wrapper(*args, **kwargs):
|
|
45
|
+
start_time = time.time()
|
|
46
|
+
|
|
47
|
+
# Execute the original function
|
|
48
|
+
result = func(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
end_time = time.time()
|
|
51
|
+
total_seconds = end_time - start_time
|
|
52
|
+
minutes, seconds = divmod(total_seconds, 60)
|
|
53
|
+
|
|
54
|
+
print(f"\n{BLUE}======================================================{NC}")
|
|
55
|
+
print(f"{BLUE}Total time taken: {int(minutes)} minute(s) and {seconds:.2f} seconds.{NC}")
|
|
56
|
+
print(f"{BLUE}======================================================{NC}")
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
return wrapper
|
|
60
|
+
|
|
61
|
+
# ============================================================================
|
|
62
|
+
# LOADING SPINNER
|
|
63
|
+
# ============================================================================
|
|
64
|
+
|
|
65
|
+
def show_loading(description, process):
|
|
66
|
+
"""
|
|
67
|
+
Displays a loading spinner with a custom message while a process is running
|
|
68
|
+
Parameters:
|
|
69
|
+
description: Description message to display
|
|
70
|
+
process: Process object to monitor
|
|
71
|
+
"""
|
|
72
|
+
spinner_index = 0
|
|
73
|
+
braille_spinner_list = '⡿⣟⣯⣷⣾⣽⣻⢿'
|
|
74
|
+
print(description, end='', flush=True)
|
|
75
|
+
# Continue spinning while the process is running
|
|
76
|
+
while process.poll() is None:
|
|
77
|
+
print(f"\b{MAGENTA}{braille_spinner_list[spinner_index]}{NC}", end='', flush=True)
|
|
78
|
+
spinner_index = (spinner_index + 1) % len(braille_spinner_list)
|
|
79
|
+
time.sleep(0.025)
|
|
80
|
+
stdout, stderr = process.communicate()
|
|
81
|
+
# Display success or failure icon based on the process exit status
|
|
82
|
+
if process.returncode == 0:
|
|
83
|
+
print(f"\b{CHECKMARK} ", flush=True)
|
|
84
|
+
return True
|
|
85
|
+
else:
|
|
86
|
+
print(f"\b{CROSS} ", flush=True)
|
|
87
|
+
if stdout:
|
|
88
|
+
print(f"\n{GREEN}Output:\n{stdout}{NC}")
|
|
89
|
+
if stderr:
|
|
90
|
+
print(f"\n{RED}Error Output:\n{stderr}{NC}")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# ============================================================================
|
|
94
|
+
# COMMAND EXECUTION WITH SPINNER
|
|
95
|
+
# ============================================================================
|
|
96
|
+
|
|
97
|
+
def run_command_with_spinner(cmd_list, description):
|
|
98
|
+
"""
|
|
99
|
+
Runs a command with a loading spinner
|
|
100
|
+
Parameters:
|
|
101
|
+
cmd_list: List of command arguments
|
|
102
|
+
description: Description to show with spinner
|
|
103
|
+
Returns:
|
|
104
|
+
True if successful, False otherwise
|
|
105
|
+
"""
|
|
106
|
+
shell_needed = platform.system() == "Windows"
|
|
107
|
+
|
|
108
|
+
process = subprocess.Popen(
|
|
109
|
+
cmd_list,
|
|
110
|
+
stdout=subprocess.PIPE,
|
|
111
|
+
stderr=subprocess.PIPE,
|
|
112
|
+
shell=shell_needed,
|
|
113
|
+
encoding='utf-8', # Fix Windows encoding issue
|
|
114
|
+
errors='replace' # Replace problematic characters instead of crashing
|
|
115
|
+
)
|
|
116
|
+
return show_loading(description, process)
|
|
117
|
+
|
|
118
|
+
# ============================================================================
|
|
119
|
+
# ENVIRONMENT FILE OPERATIONS
|
|
120
|
+
# ============================================================================
|
|
121
|
+
|
|
122
|
+
def read_env_value(key, env_file=".env"):
|
|
123
|
+
"""
|
|
124
|
+
Read a value from .env file
|
|
125
|
+
Parameters:
|
|
126
|
+
key: Environment variable key to read
|
|
127
|
+
env_file: Path to .env file (default: ".env")
|
|
128
|
+
Returns:
|
|
129
|
+
Value of the key or None if not found
|
|
130
|
+
"""
|
|
131
|
+
if not os.path.exists(env_file):
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
with open(env_file, 'r', encoding='utf-8') as f:
|
|
135
|
+
for line in f:
|
|
136
|
+
line = line.strip()
|
|
137
|
+
if line.startswith(f"{key}="):
|
|
138
|
+
return line.split('=', 1)[1]
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
def update_env_value(key, value, env_file=".env"):
|
|
142
|
+
"""
|
|
143
|
+
Update a value in .env file
|
|
144
|
+
Parameters:
|
|
145
|
+
key: Environment variable key to update
|
|
146
|
+
value: New value to set
|
|
147
|
+
env_file: Path to .env file (default: ".env")
|
|
148
|
+
Returns:
|
|
149
|
+
True if successful, False otherwise
|
|
150
|
+
"""
|
|
151
|
+
if not os.path.exists(env_file):
|
|
152
|
+
print(f"{RED}❌ .env file not found!{NC}")
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
with open(env_file, 'r', encoding='utf-8') as f:
|
|
156
|
+
content = f.read()
|
|
157
|
+
|
|
158
|
+
# Replace the value using regex
|
|
159
|
+
pattern = f"^{key}=.*$"
|
|
160
|
+
replacement = f"{key}={value}"
|
|
161
|
+
new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
|
|
162
|
+
|
|
163
|
+
with open(env_file, 'w', encoding='utf-8') as f:
|
|
164
|
+
f.write(new_content)
|
|
165
|
+
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
# ============================================================================
|
|
169
|
+
# PLATFORM DETECTION
|
|
170
|
+
# ============================================================================
|
|
171
|
+
|
|
172
|
+
def is_windows():
|
|
173
|
+
"""Check if running on Windows"""
|
|
174
|
+
return platform.system() == "Windows"
|
|
175
|
+
|
|
176
|
+
def is_macos():
|
|
177
|
+
"""Check if running on macOS"""
|
|
178
|
+
return platform.system() == "Darwin"
|
|
179
|
+
|
|
180
|
+
def is_linux():
|
|
181
|
+
"""Check if running on Linux"""
|
|
182
|
+
return platform.system() == "Linux"
|
|
183
|
+
|
|
184
|
+
def get_user_shell():
|
|
185
|
+
"""
|
|
186
|
+
Detect user's shell (zsh, bash, fish, etc.)
|
|
187
|
+
Returns:
|
|
188
|
+
Tuple of (shell_name, config_file_path)
|
|
189
|
+
"""
|
|
190
|
+
home = Path.home()
|
|
191
|
+
shell_env = os.environ.get('SHELL', '')
|
|
192
|
+
|
|
193
|
+
# Detect shell type
|
|
194
|
+
if 'zsh' in shell_env:
|
|
195
|
+
return ('zsh', home / '.zshrc')
|
|
196
|
+
elif 'bash' in shell_env:
|
|
197
|
+
# Check if .bashrc or .bash_profile exists
|
|
198
|
+
if (home / '.bashrc').exists():
|
|
199
|
+
return ('bash', home / '.bashrc')
|
|
200
|
+
else:
|
|
201
|
+
return ('bash', home / '.bash_profile')
|
|
202
|
+
elif 'fish' in shell_env:
|
|
203
|
+
return ('fish', home / '.config/fish/config.fish')
|
|
204
|
+
else:
|
|
205
|
+
# Default to bash
|
|
206
|
+
return ('bash', home / '.bashrc')
|
|
207
|
+
|
|
208
|
+
# ============================================================================
|
|
209
|
+
# FILE/DIRECTORY OPERATIONS
|
|
210
|
+
# ============================================================================
|
|
211
|
+
|
|
212
|
+
def open_file_with_default_app(file_path):
|
|
213
|
+
"""
|
|
214
|
+
Open file with default application based on platform
|
|
215
|
+
Parameters:
|
|
216
|
+
file_path: Path to the file to open
|
|
217
|
+
Returns:
|
|
218
|
+
True if successful, False otherwise
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
if is_macos():
|
|
222
|
+
subprocess.run(['open', file_path])
|
|
223
|
+
elif is_linux():
|
|
224
|
+
subprocess.run(['xdg-open', file_path])
|
|
225
|
+
elif is_windows():
|
|
226
|
+
subprocess.run(['notepad', file_path])
|
|
227
|
+
else:
|
|
228
|
+
print(f"{YELLOW}Please manually open: {file_path}{NC}")
|
|
229
|
+
return False
|
|
230
|
+
return True
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"{RED}Error opening file: {e}{NC}")
|
|
233
|
+
print(f"{YELLOW}Please manually open: {file_path}{NC}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def open_directory(directory_path):
|
|
237
|
+
"""
|
|
238
|
+
Opens a directory based on the operating system
|
|
239
|
+
Parameters:
|
|
240
|
+
directory_path: Path to the directory to open
|
|
241
|
+
Returns:
|
|
242
|
+
True if successful, False otherwise
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
if is_macos():
|
|
246
|
+
subprocess.run(["open", directory_path])
|
|
247
|
+
elif is_linux():
|
|
248
|
+
subprocess.run(["xdg-open", directory_path])
|
|
249
|
+
elif is_windows():
|
|
250
|
+
# Convert to absolute path and use Windows path separators
|
|
251
|
+
abs_path = os.path.abspath(directory_path)
|
|
252
|
+
# Use explorer to open the directory
|
|
253
|
+
subprocess.run(["explorer", abs_path], shell=True)
|
|
254
|
+
else:
|
|
255
|
+
print(f"{YELLOW}Cannot open directory automatically. Please check: {directory_path}{NC}")
|
|
256
|
+
return False
|
|
257
|
+
return True
|
|
258
|
+
except Exception as e:
|
|
259
|
+
print(f"{RED}Error opening directory: {e}{NC}")
|
|
260
|
+
print(f"{YELLOW}Please check: {directory_path}{NC}")
|
|
261
|
+
return False
|
core/__init__.py
ADDED
core/constants.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Constants and compiled regex patterns
|
|
4
|
+
"""
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Regex patterns (compiled once)
|
|
9
|
+
PATTERNS = {
|
|
10
|
+
'package_name_kts': re.compile(r'applicationId\s*=\s*["\']([^"\']+)["\']'),
|
|
11
|
+
'package_name_groovy': re.compile(r'applicationId\s+["\']([^"\']+)["\']'),
|
|
12
|
+
'app_label': re.compile(r'android:label="([^"]+)"'),
|
|
13
|
+
'architecture': re.compile(r'(arm64-v8a|armeabi-v7a|x86|x86_64)'),
|
|
14
|
+
'version': re.compile(r'^version:\s*(.+)$', re.MULTILINE),
|
|
15
|
+
'version_with_build': re.compile(r'^version:\s*["\']?([\d\.]+)\+(\d+)["\']?', re.MULTILINE),
|
|
16
|
+
'version_line': re.compile(r'^version:\s*["\']?[\d\.]+(\+\d+)?["\']?', re.MULTILINE),
|
|
17
|
+
'ip_address': re.compile(r'inet (\d+\.\d+\.\d+\.\d+)'),
|
|
18
|
+
'foreground_app': re.compile(r'u0 ([^/\s]+)'),
|
|
19
|
+
'resumed_activity': re.compile(r'mResumedActivity.*?{.*?u0\s+(\S+?)/'),
|
|
20
|
+
'top_resumed_activity': re.compile(r'(?:top)?ResumedActivity.*?u0\s+(\S+?)/'),
|
|
21
|
+
'sanitize_special': re.compile(r'[<>:"/\\|?*]'),
|
|
22
|
+
'sanitize_non_word': re.compile(r'[^\w\s-]'),
|
|
23
|
+
'sanitize_spaces': re.compile(r'[-\s]+'),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Path constants
|
|
27
|
+
PATHS = {
|
|
28
|
+
'gradle_kts': Path("android/app/build.gradle.kts"),
|
|
29
|
+
'gradle': Path("android/app/build.gradle"),
|
|
30
|
+
'manifest': Path("android/app/src/main/AndroidManifest.xml"),
|
|
31
|
+
'pubspec': Path("pubspec.yaml"),
|
|
32
|
+
'info_plist': Path("ios/Runner/Info.plist"),
|
|
33
|
+
'apk_output': Path("build/app/outputs/flutter-apk"),
|
|
34
|
+
'aab_output': Path("build/app/outputs/bundle/release"),
|
|
35
|
+
'ipa_output': Path("build/ios/ipa"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Build commands
|
|
39
|
+
BUILD_COMMANDS = {
|
|
40
|
+
'apk': [
|
|
41
|
+
"flutter", "build", "apk", "--release", "--obfuscate",
|
|
42
|
+
"--target-platform", "android-arm64", "--split-debug-info=./"
|
|
43
|
+
],
|
|
44
|
+
'apk_split': [
|
|
45
|
+
"flutter", "build", "apk", "--release", "--split-per-abi",
|
|
46
|
+
"--obfuscate", "--split-debug-info=./"
|
|
47
|
+
],
|
|
48
|
+
'aab': [
|
|
49
|
+
"flutter", "build", "appbundle", "--release",
|
|
50
|
+
"--obfuscate", "--split-debug-info=./"
|
|
51
|
+
],
|
|
52
|
+
'ipa': [
|
|
53
|
+
"flutter", "build", "ipa", "--release",
|
|
54
|
+
"--obfuscate", "--split-debug-info=./",
|
|
55
|
+
"--export-method", "app-store"
|
|
56
|
+
],
|
|
57
|
+
}
|
core/state.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared state management for flutter-dev tools
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Global variable to store selected Android device
|
|
7
|
+
_SELECTED_DEVICE = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_selected_device():
|
|
11
|
+
"""Get the currently selected device"""
|
|
12
|
+
global _SELECTED_DEVICE
|
|
13
|
+
return _SELECTED_DEVICE
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_selected_device(device):
|
|
17
|
+
"""Set the selected device"""
|
|
18
|
+
global _SELECTED_DEVICE
|
|
19
|
+
_SELECTED_DEVICE = device
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def clear_selected_device():
|
|
23
|
+
"""Clear the selected device"""
|
|
24
|
+
global _SELECTED_DEVICE
|
|
25
|
+
_SELECTED_DEVICE = None
|
create_page.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
RED = '\033[0;31m'
|
|
8
|
+
GREEN = '\033[0;32m'
|
|
9
|
+
YELLOW = '\033[1;33m'
|
|
10
|
+
BLUE = '\033[0;34m'
|
|
11
|
+
NC = '\033[0m'
|
|
12
|
+
|
|
13
|
+
def get_project_name():
|
|
14
|
+
"""Get the project name from pubspec.yaml using regex"""
|
|
15
|
+
if os.path.isfile("pubspec.yaml"):
|
|
16
|
+
with open("pubspec.yaml", 'r') as file:
|
|
17
|
+
try:
|
|
18
|
+
content = file.read()
|
|
19
|
+
# Use regex to find the name field in pubspec.yaml
|
|
20
|
+
name_match = re.search(r'^name:\s*(.+)$', content, re.MULTILINE)
|
|
21
|
+
if name_match:
|
|
22
|
+
project_name = name_match.group(1).strip()
|
|
23
|
+
# Remove quotes if present
|
|
24
|
+
project_name = project_name.strip('"\'')
|
|
25
|
+
return project_name
|
|
26
|
+
else:
|
|
27
|
+
print(f"{RED}Error: Could not find 'name' field in pubspec.yaml.{NC}")
|
|
28
|
+
exit(1)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f"{RED}Error: Could not read pubspec.yaml: {e}{NC}")
|
|
31
|
+
exit(1)
|
|
32
|
+
else:
|
|
33
|
+
print(f"{RED}Error: pubspec.yaml not found in the current directory.{NC}")
|
|
34
|
+
print(f"{YELLOW}Please run this command from the root of a Flutter project.{NC}")
|
|
35
|
+
exit(1)
|
|
36
|
+
|
|
37
|
+
def update_service_locator(project_name, class_prefix, page_name):
|
|
38
|
+
"""Update service_locator.dart with the new feature DI"""
|
|
39
|
+
service_locator_path = "lib/core/di/service_locator.dart"
|
|
40
|
+
|
|
41
|
+
if not os.path.isfile(service_locator_path):
|
|
42
|
+
print(f"{YELLOW}Warning: Could not find service_locator.dart at {service_locator_path}.{NC}")
|
|
43
|
+
print(f"{YELLOW}Feature DI registration in service locator skipped.{NC}")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
with open(service_locator_path, 'r') as file:
|
|
47
|
+
content = file.read()
|
|
48
|
+
|
|
49
|
+
# Add import statement
|
|
50
|
+
import_statement = f"import 'package:{project_name}/features/{page_name}/di/{page_name}_di.dart';"
|
|
51
|
+
if import_statement not in content:
|
|
52
|
+
# Find the last import statement and add after it
|
|
53
|
+
import_matches = re.findall(r'import [^;]+;', content)
|
|
54
|
+
if import_matches:
|
|
55
|
+
last_import = import_matches[-1]
|
|
56
|
+
content = content.replace(last_import, f"{last_import}\n{import_statement}")
|
|
57
|
+
|
|
58
|
+
# Add DI setup call in setUp method
|
|
59
|
+
di_call = f" await {class_prefix}Di.setup(_serviceLocator);"
|
|
60
|
+
|
|
61
|
+
# Find the feature DI setup comment and add after it
|
|
62
|
+
feature_comment_pattern = r'//Feature DI setup'
|
|
63
|
+
if feature_comment_pattern in content:
|
|
64
|
+
content = re.sub(
|
|
65
|
+
r'(//Feature DI setup\s*\n)',
|
|
66
|
+
f'\\1{di_call}\n',
|
|
67
|
+
content
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
# If comment doesn't exist, add before the closing brace of setUp method
|
|
71
|
+
setup_pattern = r'(\s+await\s+\w+\.setup\(_serviceLocator\);\s*\n)(\s*})'
|
|
72
|
+
match = re.search(setup_pattern, content)
|
|
73
|
+
if match:
|
|
74
|
+
content = re.sub(
|
|
75
|
+
setup_pattern,
|
|
76
|
+
f'\\1\n //Feature DI setup\n{di_call}\n\\2',
|
|
77
|
+
content
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
with open(service_locator_path, 'w') as file:
|
|
81
|
+
file.write(content)
|
|
82
|
+
|
|
83
|
+
print(f"{GREEN}✓ Updated service_locator.dart with {class_prefix}Di registration.{NC}")
|
|
84
|
+
print(f"{BLUE} Added: {NC}{di_call}")
|
|
85
|
+
|
|
86
|
+
def generate_page(page_name):
|
|
87
|
+
"""Generate Flutter feature structure and files"""
|
|
88
|
+
if not page_name:
|
|
89
|
+
print(f"{RED}Error: Page name is required.{NC}")
|
|
90
|
+
print(f"{BLUE}Usage: {sys.argv[0]} page <page_name>{NC}")
|
|
91
|
+
exit(1)
|
|
92
|
+
|
|
93
|
+
project_name = get_project_name()
|
|
94
|
+
|
|
95
|
+
# Convert page name to lowercase
|
|
96
|
+
page_name = page_name.lower()
|
|
97
|
+
|
|
98
|
+
# Create class prefix - convert snake_case to PascalCase
|
|
99
|
+
class_prefix = ''.join(word.capitalize() for word in page_name.split('_'))
|
|
100
|
+
|
|
101
|
+
print(f"{YELLOW}Creating feature structure for {class_prefix} in {project_name} project...{NC}\n")
|
|
102
|
+
|
|
103
|
+
# Create folder structure
|
|
104
|
+
base_path = f"lib/features/{page_name}"
|
|
105
|
+
os.makedirs(f"{base_path}/data/datasource", exist_ok=True)
|
|
106
|
+
os.makedirs(f"{base_path}/data/models", exist_ok=True)
|
|
107
|
+
os.makedirs(f"{base_path}/data/repositories", exist_ok=True)
|
|
108
|
+
os.makedirs(f"{base_path}/domain/datasource", exist_ok=True)
|
|
109
|
+
os.makedirs(f"{base_path}/domain/repositories", exist_ok=True)
|
|
110
|
+
os.makedirs(f"{base_path}/domain/entities", exist_ok=True)
|
|
111
|
+
os.makedirs(f"{base_path}/domain/usecase", exist_ok=True)
|
|
112
|
+
os.makedirs(f"{base_path}/presentation/presenter", exist_ok=True)
|
|
113
|
+
os.makedirs(f"{base_path}/presentation/ui", exist_ok=True)
|
|
114
|
+
os.makedirs(f"{base_path}/presentation/widgets", exist_ok=True)
|
|
115
|
+
os.makedirs(f"{base_path}/di", exist_ok=True)
|
|
116
|
+
|
|
117
|
+
# Generate Domain Repository content
|
|
118
|
+
domain_repository_content = f'''abstract class {class_prefix}Repository {{
|
|
119
|
+
|
|
120
|
+
}}
|
|
121
|
+
'''
|
|
122
|
+
|
|
123
|
+
# Generate Data Repository Implementation content
|
|
124
|
+
data_repository_content = f'''import 'package:{project_name}/features/{page_name}/domain/repositories/{page_name}_repository.dart';
|
|
125
|
+
|
|
126
|
+
class {class_prefix}RepositoryImpl implements {class_prefix}Repository {{
|
|
127
|
+
|
|
128
|
+
}}
|
|
129
|
+
'''
|
|
130
|
+
|
|
131
|
+
# Generate DI file content
|
|
132
|
+
di_content = f'''import 'package:{project_name}/core/base/base_presenter.dart';
|
|
133
|
+
import 'package:{project_name}/features/{page_name}/data/repositories/{page_name}_repository_impl.dart';
|
|
134
|
+
import 'package:{project_name}/features/{page_name}/domain/repositories/{page_name}_repository.dart';
|
|
135
|
+
import 'package:{project_name}/features/{page_name}/presentation/presenter/{page_name}_presenter.dart';
|
|
136
|
+
|
|
137
|
+
import 'package:get_it/get_it.dart';
|
|
138
|
+
|
|
139
|
+
class {class_prefix}Di {{
|
|
140
|
+
static Future<void> setup(GetIt serviceLocator) async {{
|
|
141
|
+
// Data Source
|
|
142
|
+
|
|
143
|
+
// Repository
|
|
144
|
+
serviceLocator.registerLazySingleton<{class_prefix}Repository>(
|
|
145
|
+
() => {class_prefix}RepositoryImpl(),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Use Cases
|
|
149
|
+
|
|
150
|
+
// Presenters
|
|
151
|
+
serviceLocator.registerFactory(
|
|
152
|
+
() => loadPresenter({class_prefix}Presenter()),
|
|
153
|
+
);
|
|
154
|
+
}}
|
|
155
|
+
}}
|
|
156
|
+
'''
|
|
157
|
+
|
|
158
|
+
# Generate presenter content
|
|
159
|
+
presenter_content = f'''import 'dart:async';
|
|
160
|
+
import 'package:{project_name}/core/base/base_presenter.dart';
|
|
161
|
+
import 'package:{project_name}/core/utility/navigation_helpers.dart';
|
|
162
|
+
import 'package:{project_name}/features/{page_name}/presentation/presenter/{page_name}_ui_state.dart';
|
|
163
|
+
|
|
164
|
+
class {class_prefix}Presenter extends BasePresenter<{class_prefix}UiState> {{
|
|
165
|
+
final Obs<{class_prefix}UiState> uiState = Obs<{class_prefix}UiState>({class_prefix}UiState.empty());
|
|
166
|
+
{class_prefix}UiState get currentUiState => uiState.value;
|
|
167
|
+
|
|
168
|
+
@override
|
|
169
|
+
Future<void> addUserMessage(String message) async {{
|
|
170
|
+
uiState.value = currentUiState.copyWith(userMessage: message);
|
|
171
|
+
showMessage(message: currentUiState.userMessage);
|
|
172
|
+
}}
|
|
173
|
+
|
|
174
|
+
@override
|
|
175
|
+
Future<void> toggleLoading({{required bool loading}}) async {{
|
|
176
|
+
uiState.value = currentUiState.copyWith(isLoading: loading);
|
|
177
|
+
}}
|
|
178
|
+
}}
|
|
179
|
+
'''
|
|
180
|
+
|
|
181
|
+
# Generate UI state content
|
|
182
|
+
ui_state_content = f'''import 'package:{project_name}/core/base/base_ui_state.dart';
|
|
183
|
+
|
|
184
|
+
class {class_prefix}UiState extends BaseUiState {{
|
|
185
|
+
const {class_prefix}UiState({{required super.isLoading, required super.userMessage}});
|
|
186
|
+
|
|
187
|
+
factory {class_prefix}UiState.empty() {{
|
|
188
|
+
return {class_prefix}UiState(isLoading: false, userMessage: '');
|
|
189
|
+
}}
|
|
190
|
+
|
|
191
|
+
@override
|
|
192
|
+
List<Object?> get props => [isLoading, userMessage];
|
|
193
|
+
|
|
194
|
+
//Add more properties to the state
|
|
195
|
+
|
|
196
|
+
{class_prefix}UiState copyWith({{bool? isLoading, String? userMessage}}) {{
|
|
197
|
+
return {class_prefix}UiState(
|
|
198
|
+
isLoading: isLoading ?? this.isLoading,
|
|
199
|
+
userMessage: userMessage ?? this.userMessage,
|
|
200
|
+
);
|
|
201
|
+
}}
|
|
202
|
+
}}
|
|
203
|
+
'''
|
|
204
|
+
|
|
205
|
+
# Generate page content
|
|
206
|
+
page_content = f'''import 'package:flutter/material.dart';
|
|
207
|
+
|
|
208
|
+
class {class_prefix}Page extends StatelessWidget {{
|
|
209
|
+
const {class_prefix}Page({{super.key}});
|
|
210
|
+
|
|
211
|
+
@override
|
|
212
|
+
Widget build(BuildContext context) {{
|
|
213
|
+
return Scaffold(
|
|
214
|
+
appBar: AppBar(title: Text('{class_prefix}')),
|
|
215
|
+
body: Center(child: Text('{class_prefix}')),
|
|
216
|
+
);
|
|
217
|
+
}}
|
|
218
|
+
}}
|
|
219
|
+
'''
|
|
220
|
+
|
|
221
|
+
# Write files
|
|
222
|
+
with open(f"{base_path}/domain/repositories/{page_name}_repository.dart", 'w') as file:
|
|
223
|
+
file.write(domain_repository_content)
|
|
224
|
+
|
|
225
|
+
with open(f"{base_path}/data/repositories/{page_name}_repository_impl.dart", 'w') as file:
|
|
226
|
+
file.write(data_repository_content)
|
|
227
|
+
|
|
228
|
+
with open(f"{base_path}/di/{page_name}_di.dart", 'w') as file:
|
|
229
|
+
file.write(di_content)
|
|
230
|
+
|
|
231
|
+
with open(f"{base_path}/presentation/presenter/{page_name}_presenter.dart", 'w') as file:
|
|
232
|
+
file.write(presenter_content)
|
|
233
|
+
|
|
234
|
+
with open(f"{base_path}/presentation/presenter/{page_name}_ui_state.dart", 'w') as file:
|
|
235
|
+
file.write(ui_state_content)
|
|
236
|
+
|
|
237
|
+
with open(f"{base_path}/presentation/ui/{page_name}_page.dart", 'w') as file:
|
|
238
|
+
file.write(page_content)
|
|
239
|
+
|
|
240
|
+
# Update service locator
|
|
241
|
+
update_service_locator(project_name, class_prefix, page_name)
|
|
242
|
+
|
|
243
|
+
# Print success message
|
|
244
|
+
print(f"\n{GREEN}✓ Feature '{class_prefix}' created successfully!{NC}")
|
|
245
|
+
print(f" {BLUE}Structure:{NC}")
|
|
246
|
+
print(f" └── {BLUE}lib/features/{page_name}{NC}")
|
|
247
|
+
print(f" ├── {BLUE}data{NC}")
|
|
248
|
+
print(f" │ ├── {BLUE}datasource{NC}")
|
|
249
|
+
print(f" │ └── {BLUE}repositories{NC}")
|
|
250
|
+
print(f" │ └── {GREEN}{page_name}_repository_impl.dart{NC}")
|
|
251
|
+
print(f" ├── {BLUE}domain{NC}")
|
|
252
|
+
print(f" │ ├── {BLUE}datasource{NC}")
|
|
253
|
+
print(f" │ ├── {BLUE}repositories{NC}")
|
|
254
|
+
print(f" │ │ └── {GREEN}{page_name}_repository.dart{NC}")
|
|
255
|
+
print(f" │ ├── {BLUE}entities{NC}")
|
|
256
|
+
print(f" │ └── {BLUE}usecase{NC}")
|
|
257
|
+
print(f" ├── {BLUE}presentation{NC}")
|
|
258
|
+
print(f" │ ├── {BLUE}presenter{NC}")
|
|
259
|
+
print(f" │ │ ├── {GREEN}{page_name}_presenter.dart{NC}")
|
|
260
|
+
print(f" │ │ └── {GREEN}{page_name}_ui_state.dart{NC}")
|
|
261
|
+
print(f" │ ├── {BLUE}ui{NC}")
|
|
262
|
+
print(f" │ │ └── {GREEN}{page_name}_page.dart{NC}")
|
|
263
|
+
print(f" │ └── {BLUE}widgets{NC}")
|
|
264
|
+
print(f" └── {BLUE}di{NC}")
|
|
265
|
+
print(f" └── {GREEN}{page_name}_di.dart{NC}")
|
|
266
|
+
|
|
267
|
+
def main():
|
|
268
|
+
if len(sys.argv) < 3:
|
|
269
|
+
print(f"{RED}Error: Insufficient arguments.{NC}")
|
|
270
|
+
print(f"{BLUE}Usage: {sys.argv[0]} page <page_name>{NC}")
|
|
271
|
+
exit(1)
|
|
272
|
+
|
|
273
|
+
command = sys.argv[1].lower()
|
|
274
|
+
|
|
275
|
+
if command == "page":
|
|
276
|
+
if len(sys.argv) >= 3:
|
|
277
|
+
generate_page(sys.argv[2])
|
|
278
|
+
else:
|
|
279
|
+
print(f"{RED}Error: Page name is required.{NC}")
|
|
280
|
+
print(f"{BLUE}Usage: {sys.argv[0]} page <page_name>{NC}")
|
|
281
|
+
exit(1)
|
|
282
|
+
else:
|
|
283
|
+
print(f"{RED}Error: Unknown command '{command}'.{NC}")
|
|
284
|
+
print(f"{BLUE}Available commands: page{NC}")
|
|
285
|
+
exit(1)
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
main()
|