Initial commit
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# WooCommerce Inventory Manager
|
||||
|
||||
A modern web application to manage your WooCommerce products, inventory, and plugin metadata.
|
||||
|
||||
## Prerequisites
|
||||
- Python 3.8+
|
||||
- Node.js 16+
|
||||
- WooCommerce Store with REST API enabled (Consumer Key & Secret required)
|
||||
|
||||
## Setup & Run
|
||||
|
||||
### 1. Start the Backend
|
||||
The backend runs on Python/FastAPI.
|
||||
|
||||
```bash
|
||||
# Open a new terminal
|
||||
pip install -r backend/requirements.txt
|
||||
cd backend
|
||||
python main.py
|
||||
```
|
||||
Server will start at `http://0.0.0.0:8000`.
|
||||
|
||||
### 2. Start the Frontend (Flutter)
|
||||
The frontend is a unified Flutter app for Web, Android, and Windows.
|
||||
|
||||
```bash
|
||||
# Open a new terminal
|
||||
cd mobile_app
|
||||
flutter pub get
|
||||
flutter run -d chrome # For Web
|
||||
# OR
|
||||
flutter run -d windows # For PC
|
||||
# OR
|
||||
flutter run -d android # For Mobile
|
||||
```
|
||||
|
||||
> **Note**: Logic for connecting to the backend varies by platform:
|
||||
> * **Web/Windows**: Use `localhost:8000`
|
||||
> * **Android Emulator**: Use `10.0.2.2:8000`
|
||||
> * **Physical Device**: Use your computer's local IP (e.g., `192.168.1.X:8000`)
|
||||
|
||||
## 📂 Legacy Frontend
|
||||
The old React frontend has been moved to `legacy_frontend/` and is deprecated.
|
||||
|
||||
## Features
|
||||
- **Dashboard**: View all products, search, and manage stock.
|
||||
- **Product Editor**:
|
||||
- Edit standard fields (Price, SKU, Stock).
|
||||
- **Images**: Drag & Drop image upload.
|
||||
- **Meta Data**: View and edit hidden metadata used by plugins (e.g., Yoast, Bundles).
|
||||
- **Authentication**: Secure login using your WooCommerce Consumer Key & Secret.
|
||||
|
||||
## Troubleshooting
|
||||
- If you see "CORS" errors, ensure the backend is running.
|
||||
- If images fail to upload, check your WordPress media permissions.
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
from fastapi import FastAPI, HTTPException, Body, File, UploadFile, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
import os
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
import logging
|
||||
|
||||
# Configure logging to file
|
||||
logging.basicConfig(
|
||||
filename='debug.log',
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, replace with specific origin
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
logger.info(f"Incoming request: {request.method} {request.url}")
|
||||
try:
|
||||
response = await call_next(request)
|
||||
logger.info(f"Response status: {response.status_code}")
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Request failed: {str(e)}")
|
||||
raise e
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
url: str
|
||||
consumer_key: str
|
||||
consumer_secret: str
|
||||
|
||||
class Product(BaseModel):
|
||||
name: str
|
||||
type: str = "simple"
|
||||
regular_price: str = ""
|
||||
description: str = ""
|
||||
short_description: str = ""
|
||||
categories: List[Dict[str, Any]] = []
|
||||
images: List[Dict[str, Any]] = []
|
||||
meta_data: List[Dict[str, Any]] = []
|
||||
manage_stock: bool = False
|
||||
stock_quantity: Optional[int] = None
|
||||
stock_status: str = "instock"
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
return {"message": "WooCommerce Inventory API is running"}
|
||||
|
||||
@app.post("/login")
|
||||
def login(request: LoginRequest):
|
||||
logger.info(f"Login attempt for URL: {request.url}")
|
||||
# Verify credentials by making a lightweight call to the API
|
||||
try:
|
||||
# Ensure URL has protocol
|
||||
base_url = request.url.strip()
|
||||
if not base_url.startswith('http'):
|
||||
base_url = f'https://{base_url}'
|
||||
|
||||
url = f"{base_url.rstrip('/')}/wp-json/wc/v3/system_status"
|
||||
logger.debug(f"Connecting to: {url}")
|
||||
|
||||
# Add User-Agent to avoid blocking by security plugins
|
||||
headers = {
|
||||
'User-Agent': 'WooCommerce-Inventory-App/1.0'
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
url,
|
||||
auth=HTTPBasicAuth(request.consumer_key, request.consumer_secret),
|
||||
headers=headers
|
||||
)
|
||||
logger.info(f"WooCommerce API Response: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
return {"status": "success", "message": "Login successful"}
|
||||
else:
|
||||
logger.warning(f"Login failed. Status: {response.status_code}, Body: {response.text}")
|
||||
# Try to return a helpful error message
|
||||
try:
|
||||
error_detail = response.json().get('message', response.text)
|
||||
except:
|
||||
error_detail = response.text
|
||||
raise HTTPException(status_code=401, detail=f"WooCommerce Error: {error_detail}")
|
||||
|
||||
except HTTPException as he:
|
||||
raise he
|
||||
except Exception as e:
|
||||
logger.error(f"Login exception: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=f"Connection failed: {str(e)}")
|
||||
|
||||
@app.get("/products")
|
||||
def get_products(
|
||||
url: str,
|
||||
consumer_key: str,
|
||||
consumer_secret: str,
|
||||
page: int = 1,
|
||||
per_page: int = 20,
|
||||
search: Optional[str] = None
|
||||
):
|
||||
try:
|
||||
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products"
|
||||
params = {
|
||||
"page": page,
|
||||
"per_page": per_page
|
||||
}
|
||||
if search:
|
||||
params["search"] = search
|
||||
|
||||
response = requests.get(
|
||||
api_url,
|
||||
auth=HTTPBasicAuth(consumer_key, consumer_secret),
|
||||
params=params
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/products/{product_id}")
|
||||
def get_product(
|
||||
product_id: int,
|
||||
url: str,
|
||||
consumer_key: str,
|
||||
consumer_secret: str
|
||||
):
|
||||
try:
|
||||
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products/{product_id}"
|
||||
response = requests.get(
|
||||
api_url,
|
||||
auth=HTTPBasicAuth(consumer_key, consumer_secret)
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/products")
|
||||
def create_product(
|
||||
url: str,
|
||||
consumer_key: str,
|
||||
consumer_secret: str,
|
||||
product: Dict[str, Any] = Body(...)
|
||||
):
|
||||
try:
|
||||
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products"
|
||||
response = requests.post(
|
||||
api_url,
|
||||
auth=HTTPBasicAuth(consumer_key, consumer_secret),
|
||||
json=product
|
||||
)
|
||||
if response.status_code == 201:
|
||||
return response.json()
|
||||
else:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.put("/products/{product_id}")
|
||||
def update_product(
|
||||
product_id: int,
|
||||
url: str,
|
||||
consumer_key: str,
|
||||
consumer_secret: str,
|
||||
product: Dict[str, Any] = Body(...)
|
||||
):
|
||||
try:
|
||||
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products/{product_id}"
|
||||
response = requests.put(
|
||||
api_url,
|
||||
auth=HTTPBasicAuth(consumer_key, consumer_secret),
|
||||
json=product
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.delete("/products/{product_id}")
|
||||
def delete_product(
|
||||
product_id: int,
|
||||
url: str,
|
||||
consumer_key: str,
|
||||
consumer_secret: str,
|
||||
force: bool = True
|
||||
):
|
||||
try:
|
||||
api_url = f"{url.rstrip('/')}/wp-json/wc/v3/products/{product_id}"
|
||||
params = {"force": str(force).lower()}
|
||||
response = requests.delete(
|
||||
api_url,
|
||||
auth=HTTPBasicAuth(consumer_key, consumer_secret),
|
||||
params=params
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/upload")
|
||||
async def upload_image(
|
||||
url: str,
|
||||
consumer_key: str,
|
||||
consumer_secret: str,
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
# Uploading images to WooCommerce via API is a bit different, typically POST to /wp-json/wp/v2/media
|
||||
try:
|
||||
api_url = f"{url.rstrip('/')}/wp-json/wp/v2/media"
|
||||
|
||||
# Read file content
|
||||
content = await file.read()
|
||||
|
||||
headers = {
|
||||
"Content-Disposition": f"attachment; filename={file.filename}",
|
||||
"Content-Type": file.content_type
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
api_url,
|
||||
auth=HTTPBasicAuth(consumer_key, consumer_secret),
|
||||
data=content,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
return response.json()
|
||||
else:
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
fastapi
|
||||
uvicorn
|
||||
requests
|
||||
python-multipart
|
||||
python-dotenv
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
# Configuration
|
||||
CK = 'ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317'
|
||||
CS = 'cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625'
|
||||
BASE_URL = 'https://kellcreations.com/wp-json/wc/v3'
|
||||
|
||||
def get_products():
|
||||
endpoint = f'{BASE_URL}/products'
|
||||
# Try fetching up to 20 products
|
||||
params = {
|
||||
'per_page': 20
|
||||
}
|
||||
|
||||
print(f"Connecting to {endpoint}...")
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
endpoint,
|
||||
auth=HTTPBasicAuth(CK, CS),
|
||||
params=params
|
||||
)
|
||||
|
||||
print(f"Status Code: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
products = response.json()
|
||||
print(f"Found {len(products)} products:")
|
||||
print("-" * 50)
|
||||
for product in products:
|
||||
name = product.get('name', 'N/A')
|
||||
stock_status = product.get('stock_status', 'N/A')
|
||||
stock_quantity = product.get('stock_quantity', 'N/A')
|
||||
price = product.get('price', 'N/A')
|
||||
print(f"Name: {name}")
|
||||
print(f" Price: {price}")
|
||||
print(f" Stock: {stock_status} ({stock_quantity})")
|
||||
print("-" * 50)
|
||||
else:
|
||||
print("Failed to retrieve products.")
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
get_products()
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
2025-12-16 06:12:49,426 - asyncio - DEBUG - Using proactor: IocpProactor
|
||||
2025-12-16 06:13:47,570 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
|
||||
2025-12-16 06:13:47,573 - __main__ - INFO - Login attempt for URL: https://kellcreations.com/wp-json/wc/v3
|
||||
2025-12-16 06:13:47,573 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/wp-json/wc/v3/system_status
|
||||
2025-12-16 06:13:47,575 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:13:51,944 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/wp-json/wc/v3/system_status HTTP/1.1" 404 117
|
||||
2025-12-16 06:13:51,945 - __main__ - INFO - WooCommerce API Response: 404
|
||||
2025-12-16 06:13:51,945 - __main__ - WARNING - Login failed. Status: 404, Body: {"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}
|
||||
2025-12-16 06:13:51,945 - __main__ - ERROR - Login exception: 401: Invalid credentials or URL
|
||||
2025-12-16 06:13:51,946 - __main__ - INFO - Response status: 400
|
||||
2025-12-16 06:16:16,073 - asyncio - DEBUG - Using proactor: IocpProactor
|
||||
2025-12-16 06:16:26,288 - __main__ - INFO - Incoming request: OPTIONS http://localhost:8000/login
|
||||
2025-12-16 06:16:26,288 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:16:26,291 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
|
||||
2025-12-16 06:16:26,294 - __main__ - INFO - Login attempt for URL: https://kellcreations.com/wp-json/wc/v3
|
||||
2025-12-16 06:16:26,294 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/wp-json/wc/v3/system_status
|
||||
2025-12-16 06:16:26,295 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:16:29,485 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/wp-json/wc/v3/system_status HTTP/1.1" 404 117
|
||||
2025-12-16 06:16:29,485 - __main__ - INFO - WooCommerce API Response: 404
|
||||
2025-12-16 06:16:29,486 - __main__ - WARNING - Login failed. Status: 404, Body: {"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}
|
||||
2025-12-16 06:16:29,486 - __main__ - INFO - Response status: 401
|
||||
2025-12-16 06:17:13,776 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
|
||||
2025-12-16 06:17:13,777 - __main__ - INFO - Login attempt for URL: https://kellcreations.com
|
||||
2025-12-16 06:17:13,777 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/system_status
|
||||
2025-12-16 06:17:13,778 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:17:17,649 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/system_status HTTP/1.1" 200 6831
|
||||
2025-12-16 06:17:17,650 - __main__ - INFO - WooCommerce API Response: 200
|
||||
2025-12-16 06:17:17,651 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:17:17,712 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:17:17,739 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:17:23,369 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:17:23,483 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:17:23,487 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:17:23,488 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:17:28,122 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:17:29,200 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:19:42,438 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:19:42,589 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:19:46,914 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6617
|
||||
2025-12-16 06:19:46,920 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:19:46,923 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:19:46,924 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:19:50,315 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6618
|
||||
2025-12-16 06:19:50,321 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:23:13,565 - __main__ - INFO - Incoming request: POST http://localhost:8000/login
|
||||
2025-12-16 06:23:13,566 - __main__ - INFO - Login attempt for URL: https://kellcreations.com
|
||||
2025-12-16 06:23:13,566 - __main__ - DEBUG - Connecting to: https://kellcreations.com/wp-json/wc/v3/system_status
|
||||
2025-12-16 06:23:13,567 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:23:16,821 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/system_status HTTP/1.1" 200 6831
|
||||
2025-12-16 06:23:16,821 - __main__ - INFO - WooCommerce API Response: 200
|
||||
2025-12-16 06:23:16,823 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:24:09,346 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:24:09,348 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:24:14,129 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:24:14,250 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:24:14,254 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:24:14,256 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:24:19,343 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:24:19,460 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:24:58,327 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:24:58,329 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:25:03,169 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6618
|
||||
2025-12-16 06:25:03,175 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:25:03,178 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250214?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:25:03,179 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:25:06,907 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250214 HTTP/1.1" 200 6618
|
||||
2025-12-16 06:25:06,913 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:26:31,062 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:26:31,063 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:26:35,770 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:26:36,868 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:26:36,871 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:26:36,872 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:26:40,470 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250076?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:26:40,472 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:26:41,600 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:26:42,669 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:26:44,474 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250076 HTTP/1.1" 200 4894
|
||||
2025-12-16 06:26:44,478 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:26:44,480 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/250076?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:26:44,482 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:26:47,481 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/250076 HTTP/1.1" 200 4897
|
||||
2025-12-16 06:26:47,484 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:27:25,702 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:27:26,196 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:27:31,008 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:27:32,079 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:27:32,081 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:27:32,083 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:27:36,676 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:27:37,770 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:27:42,047 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=2&search=
|
||||
2025-12-16 06:27:42,048 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:27:47,834 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=2&per_page=20 HTTP/1.1" 200 39630
|
||||
2025-12-16 06:27:47,888 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:28:10,795 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/249378?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:28:10,796 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:28:14,227 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/249378 HTTP/1.1" 200 6421
|
||||
2025-12-16 06:28:14,233 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:28:14,235 - __main__ - INFO - Incoming request: GET http://localhost:8000/products/249378?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625
|
||||
2025-12-16 06:28:14,237 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:28:17,396 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products/249378 HTTP/1.1" 200 6421
|
||||
2025-12-16 06:28:17,402 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:29:22,094 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:29:22,095 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:29:26,351 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:29:27,421 - __main__ - INFO - Response status: 200
|
||||
2025-12-16 06:29:27,423 - __main__ - INFO - Incoming request: GET http://localhost:8000/products?url=https:%2F%2Fkellcreations.com&consumer_key=ck_d60ea51ceb4a399d3dadd6307d1227a8768f3317&consumer_secret=cs_df2615caa97a4ca1c1a02f1b512c10d9770fb625&page=1&search=
|
||||
2025-12-16 06:29:27,425 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): kellcreations.com:443
|
||||
2025-12-16 06:29:32,212 - urllib3.connectionpool - DEBUG - https://kellcreations.com:443 "GET /wp-json/wc/v3/products?page=1&per_page=20 HTTP/1.1" 200 None
|
||||
2025-12-16 06:29:33,283 - __main__ - INFO - Response status: 200
|
||||
2025-12-25 18:12:03,135 - asyncio - DEBUG - Using proactor: IocpProactor
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"axios": "^1.13.2",
|
||||
"lucide-react": "^0.561.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.10.1",
|
||||
"react-toastify": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,42 @@
|
|||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import Login from './components/Login';
|
||||
import ProductList from './components/ProductList';
|
||||
import ProductEditor from './components/ProductEditor';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="/dashboard" element={<ProductList />} />
|
||||
<Route path="/product/:id" element={<ProductEditor />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const API_URL = 'http://localhost:8000'; // Adjust if backend runs elsewhere
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export const login = async (url, consumer_key, consumer_secret) => {
|
||||
const response = await api.post('/login', { url, consumer_key, consumer_secret });
|
||||
// Store creds in localStorage for convenience in this demo app
|
||||
localStorage.setItem('wc_creds', JSON.stringify({ url, consumer_key, consumer_secret }));
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getStoredCreds = () => {
|
||||
const stored = localStorage.getItem('wc_creds');
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
localStorage.removeItem('wc_creds');
|
||||
};
|
||||
|
||||
export const getProducts = async (page = 1, search = '') => {
|
||||
const creds = getStoredCreds();
|
||||
if (!creds) throw new Error('Not logged in');
|
||||
|
||||
const response = await api.get('/products', {
|
||||
params: {
|
||||
...creds,
|
||||
page,
|
||||
search
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getProduct = async (id) => {
|
||||
const creds = getStoredCreds();
|
||||
if (!creds) throw new Error('Not logged in');
|
||||
|
||||
const response = await api.get(`/products/${id}`, {
|
||||
params: { ...creds }
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createProduct = async (productData) => {
|
||||
const creds = getStoredCreds();
|
||||
if (!creds) throw new Error('Not logged in');
|
||||
|
||||
const response = await api.post('/products', productData, {
|
||||
params: { ...creds }
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateProduct = async (id, productData) => {
|
||||
const creds = getStoredCreds();
|
||||
if (!creds) throw new Error('Not logged in');
|
||||
|
||||
const response = await api.put(`/products/${id}`, productData, {
|
||||
params: { ...creds }
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteProduct = async (id) => {
|
||||
const creds = getStoredCreds();
|
||||
if (!creds) throw new Error('Not logged in');
|
||||
|
||||
const response = await api.delete(`/products/${id}`, {
|
||||
params: { ...creds }
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const uploadImage = async (file) => {
|
||||
const creds = getStoredCreds();
|
||||
if (!creds) throw new Error('Not logged in');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Pass creds as query params because we are sending form data body
|
||||
const response = await api.post('/upload', formData, {
|
||||
params: { ...creds },
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -0,0 +1,114 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { login } from '../api';
|
||||
import { Key, Lock, Globe } from 'lucide-react';
|
||||
|
||||
export default function Login() {
|
||||
const [url, setUrl] = useState('');
|
||||
const [ck, setCk] = useState('');
|
||||
const [cs, setCs] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault();
|
||||
console.log('Login button clicked');
|
||||
console.log('Credentials:', { url, ck, cs });
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
console.log('Calling login API...');
|
||||
const response = await login(url, ck, cs);
|
||||
console.log('Login successful:', response);
|
||||
navigate('/dashboard');
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
setError(err.response?.data?.detail || 'Login failed. Please check your credentials.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-900 px-4">
|
||||
<div className="max-w-md w-full bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-700">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">WooCommerce Manager</h1>
|
||||
<p className="text-gray-400">Enter your store details to connect</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500 text-red-200 p-3 rounded mb-6 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Store URL</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Globe className="h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
className="block w-full pl-10 bg-gray-700 border-gray-600 rounded-md py-2.5 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
|
||||
placeholder="https://example.com"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Consumer Key</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Key className="h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="block w-full pl-10 bg-gray-700 border-gray-600 rounded-md py-2.5 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
|
||||
placeholder="ck_..."
|
||||
value={ck}
|
||||
onChange={(e) => setCk(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Consumer Secret</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
className="block w-full pl-10 bg-gray-700 border-gray-600 rounded-md py-2.5 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
|
||||
placeholder="cs_..."
|
||||
value={cs}
|
||||
onChange={(e) => setCs(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-4 px-6 border border-transparent rounded-lg shadow-lg text-lg font-bold text-white uppercase tracking-wider bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 hover:shadow-xl hover:scale-[1.02] transform transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||
>
|
||||
{loading ? 'Connecting...' : 'CONNECT TO STORE'}
|
||||
</button>
|
||||
|
||||
<div className="text-xs text-gray-500 text-center mt-4">
|
||||
Ensure REST API is enabled in your WooCommerce settings.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { getProduct, createProduct, updateProduct, uploadImage } from '../api';
|
||||
import { ArrowLeft, Save, Plus, X, Upload, Image as ImageIcon, Trash2 } from 'lucide-react';
|
||||
|
||||
export default function ProductEditor() {
|
||||
const { id } = useParams();
|
||||
const isNew = id === 'new';
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(!isNew); // If new, not loading
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Product State
|
||||
const [product, setProduct] = useState({
|
||||
name: '',
|
||||
type: 'simple',
|
||||
regular_price: '',
|
||||
description: '',
|
||||
short_description: '',
|
||||
sku: '',
|
||||
manage_stock: false,
|
||||
stock_quantity: 0,
|
||||
stock_status: 'instock',
|
||||
images: [],
|
||||
meta_data: []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNew) {
|
||||
fetchProduct();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchProduct = async () => {
|
||||
try {
|
||||
const data = await getProduct(id);
|
||||
setProduct(data);
|
||||
} catch (err) {
|
||||
setError('Failed to load product');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setProduct(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleMetaChange = (index, key, value) => {
|
||||
const newMeta = [...product.meta_data];
|
||||
newMeta[index] = { ...newMeta[index], key, value };
|
||||
setProduct(prev => ({ ...prev, meta_data: newMeta }));
|
||||
};
|
||||
|
||||
const addMeta = () => {
|
||||
setProduct(prev => ({
|
||||
...prev,
|
||||
meta_data: [...prev.meta_data, { key: '', value: '' }]
|
||||
}));
|
||||
};
|
||||
|
||||
const removeMeta = (index) => {
|
||||
setProduct(prev => ({
|
||||
...prev,
|
||||
meta_data: prev.meta_data.filter((_, i) => i !== index)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleImageUpload = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setSaving(true); // Reuse saving state for upload indicator
|
||||
try {
|
||||
const media = await uploadImage(file);
|
||||
setProduct(prev => ({
|
||||
...prev,
|
||||
images: [...prev.images, { id: media.id, src: media.source_url }]
|
||||
}));
|
||||
} catch (err) {
|
||||
alert('Failed to upload image');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeImage = (index) => {
|
||||
setProduct(prev => ({
|
||||
...prev,
|
||||
images: prev.images.filter((_, i) => i !== index)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async (e) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
if (isNew) {
|
||||
await createProduct(product);
|
||||
} else {
|
||||
await updateProduct(id, product);
|
||||
}
|
||||
navigate('/dashboard');
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.detail || 'Failed to save product');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-center text-white mt-10">Loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100 p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<button onClick={() => navigate('/dashboard')} className="flex items-center text-gray-400 hover:text-white transition">
|
||||
<ArrowLeft className="h-5 w-5 mr-1" />
|
||||
Back to Dashboard
|
||||
</button>
|
||||
<h1 className="text-2xl font-bold">{isNew ? 'Create New Product' : 'Edit Product'}</h1>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition disabled:opacity-50"
|
||||
>
|
||||
<Save className="h-5 w-5 mr-2" />
|
||||
{saving ? 'Saving...' : 'Save Product'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500 text-red-200 p-4 rounded-lg mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Main Info */}
|
||||
<div className="md:col-span-2 space-y-6">
|
||||
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
|
||||
<h2 className="text-lg font-semibold mb-4 text-blue-400">General Information</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">Product Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={product.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">Regular Price</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={product.regular_price}
|
||||
onChange={(e) => handleChange('regular_price', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">SKU</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={product.sku || ''}
|
||||
onChange={(e) => handleChange('sku', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">Short Description</label>
|
||||
<textarea
|
||||
rows="3"
|
||||
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={product.short_description}
|
||||
onChange={(e) => handleChange('short_description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">Full Description</label>
|
||||
<textarea
|
||||
rows="5"
|
||||
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={product.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inventory */}
|
||||
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
|
||||
<h2 className="text-lg font-semibold mb-4 text-blue-400">Inventory</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="manage_stock"
|
||||
checked={product.manage_stock}
|
||||
onChange={(e) => handleChange('manage_stock', e.target.checked)}
|
||||
className="h-4 w-4 bg-gray-700 border-gray-600 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="manage_stock" className="ml-2 text-sm font-medium text-gray-300">Enable Stock Management</label>
|
||||
</div>
|
||||
{product.manage_stock && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">Stock Quantity</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={product.stock_quantity || 0}
|
||||
onChange={(e) => handleChange('stock_quantity', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-1">Stock Status</label>
|
||||
<select
|
||||
className="w-full bg-gray-700 border-gray-600 rounded-md p-2 text-white outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={product.stock_status}
|
||||
onChange={(e) => handleChange('stock_status', e.target.value)}
|
||||
>
|
||||
<option value="instock">In Stock</option>
|
||||
<option value="outofstock">Out of Stock</option>
|
||||
<option value="onbackorder">On Backorder</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Images */}
|
||||
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
|
||||
<h2 className="text-lg font-semibold mb-4 text-blue-400">Product Images</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{product.images.map((img, idx) => (
|
||||
<div key={idx} className="relative group">
|
||||
<img src={img.src} alt="" className="w-full h-24 object-cover rounded-md border border-gray-600" />
|
||||
<button
|
||||
onClick={() => removeImage(idx)}
|
||||
className="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-600 border-dashed rounded-lg cursor-pointer hover:bg-gray-750 transition">
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<Upload className="w-8 h-8 mb-2 text-gray-400" />
|
||||
<p className="text-xs text-gray-500">Click to upload image</p>
|
||||
</div>
|
||||
<input type="file" className="hidden" accept="image/*" onChange={handleImageUpload} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meta Data / Plugins */}
|
||||
{/* Meta Data / Plugins (Grouped) */}
|
||||
<div className="bg-gray-800 p-6 rounded-xl border border-gray-700 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold text-blue-400">Plugin Settings</h2>
|
||||
<button onClick={addMeta} className="text-blue-400 hover:text-blue-300 text-sm flex items-center">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Custom Meta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 max-h-[600px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{(() => {
|
||||
// Grouping Logic
|
||||
const groups = {
|
||||
'Yoast SEO': { prefix: '_yoast_', items: [], icon: '🔍' },
|
||||
'Product Bundles': { prefix: '_bundle', items: [], icon: '📦' },
|
||||
'Facebook': { prefix: 'fb_', items: [], icon: 'f' },
|
||||
'Google Listings': { prefix: '_wc_gla', items: [], icon: 'G' },
|
||||
'Tax Settings': { prefix: '_tax', items: [], icon: '💰' },
|
||||
'Other': { prefix: '', items: [], icon: '🔧' }
|
||||
};
|
||||
|
||||
// Helper to find readable name
|
||||
const getReadableName = (key) => {
|
||||
const names = {
|
||||
'_yoast_wpseo_title': 'SEO Title',
|
||||
'_yoast_wpseo_metadesc': 'Meta Description',
|
||||
'_yoast_wpseo_focuskw': 'Focus Keyword',
|
||||
'_regular_price': 'Regular Price (Meta)',
|
||||
'_price': 'Price (Meta)',
|
||||
'_sku': 'SKU (Meta)',
|
||||
'_tax_status': 'Tax Status',
|
||||
'_tax_class': 'Tax Class',
|
||||
'_bundle_data': 'Bundle Configuration',
|
||||
'_bundled_items': 'Bundled Item IDs'
|
||||
};
|
||||
return names[key] || key;
|
||||
};
|
||||
|
||||
// Sort items into groups
|
||||
product.meta_data.forEach((meta, idx) => {
|
||||
if (meta.key.startsWith('_')) {
|
||||
// System/Hidden keys often start with _
|
||||
}
|
||||
|
||||
let placed = false;
|
||||
for (const [groupName, config] of Object.entries(groups)) {
|
||||
if (groupName !== 'Other' && meta.key.includes(config.prefix)) {
|
||||
groups[groupName].items.push({ ...meta, originalIndex: idx });
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed) {
|
||||
groups['Other'].items.push({ ...meta, originalIndex: idx });
|
||||
}
|
||||
});
|
||||
|
||||
// Render Groups
|
||||
return Object.entries(groups).map(([groupName, config]) => {
|
||||
if (config.items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={groupName} className="bg-gray-750/50 rounded-lg p-3 border border-gray-600/50">
|
||||
<h3 className="text-sm font-bold text-gray-300 mb-3 flex items-center uppercase tracking-wider">
|
||||
<span className="mr-2 opacity-75">{config.icon}</span>
|
||||
{groupName}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{config.items.map((meta) => (
|
||||
<div key={meta.originalIndex} className="bg-gray-900/50 p-3 rounded border border-gray-700 relative group transition hover:border-blue-500/30">
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-xs font-semibold text-blue-300" title={meta.key}>
|
||||
{getReadableName(meta.key)}
|
||||
</label>
|
||||
<span className="text-[10px] text-gray-600 font-mono hidden group-hover:block">{meta.key}</span>
|
||||
</div>
|
||||
|
||||
{typeof meta.value === 'object' ? (
|
||||
<div className="text-xs text-gray-500 italic bg-gray-900 p-2 rounded">
|
||||
Complex Data (Editing disabled for safety)
|
||||
<details className="mt-1">
|
||||
<summary className="cursor-pointer hover:text-white">View JSON</summary>
|
||||
<pre className="mt-1 overflow-x-auto">{JSON.stringify(meta.value, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
) : meta.value && meta.value.length > 50 ? (
|
||||
<textarea
|
||||
className="w-full bg-gray-800 border-gray-700 rounded px-2 py-1 text-sm text-gray-200 outline-none focus:ring-1 focus:ring-blue-500 min-h-[60px]"
|
||||
value={meta.value}
|
||||
onChange={(e) => handleMetaChange(meta.originalIndex, e.target.value, meta.value)} // Corrected handler signature
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-gray-800 border-gray-700 rounded px-2 py-1 text-sm text-gray-200 outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={meta.value}
|
||||
onChange={(e) => handleMetaChange(meta.originalIndex, meta.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeMeta(meta.originalIndex)}
|
||||
className="absolute top-2 right-2 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition p-1"
|
||||
title="Remove Field"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { getProducts, deleteProduct, logout } from '../api';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Plus, Search, Trash2, Edit, LogOut, Package, Image as ImageIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
export default function ProductList() {
|
||||
const [products, setProducts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [viewMode, setViewMode] = useState('table');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchProducts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getProducts(page, search);
|
||||
setProducts(data);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err.message === 'Not logged in') {
|
||||
navigate('/');
|
||||
} else {
|
||||
setError('Failed to load products.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, [page]); // Re-fetch when page changes
|
||||
|
||||
// Search effect with debounce could be added, but manual search button for now or just simple effect
|
||||
const handleSearch = (e) => {
|
||||
e.preventDefault();
|
||||
setPage(1);
|
||||
fetchProducts();
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (window.confirm('Are you sure you want to delete this product?')) {
|
||||
try {
|
||||
await deleteProduct(id);
|
||||
fetchProducts(); // Refresh
|
||||
} catch (err) {
|
||||
alert('Failed to delete product');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||
{/* Navbar */}
|
||||
<nav className="bg-gray-800 border-b border-gray-700">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
<span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-teal-400">
|
||||
Inventory Manager
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-gray-700 rounded-lg p-1 flex">
|
||||
<button
|
||||
onClick={() => setViewMode('table')}
|
||||
className={`p-2 rounded-md transition ${viewMode === 'table' ? 'bg-gray-600 text-white shadow-sm' : 'text-gray-400 hover:text-white'}`}
|
||||
title="Table View"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded-md transition ${viewMode === 'grid' ? 'bg-gray-600 text-white shadow-sm' : 'text-gray-400 hover:text-white'}`}
|
||||
title="Grid View"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={handleLogout} className="flex items-center text-gray-400 hover:text-white transition">
|
||||
<LogOut className="h-5 w-5 mr-1" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center mb-8 gap-4">
|
||||
<form onSubmit={handleSearch} className="relative w-full sm:w-96">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search products..."
|
||||
className="block w-full pl-10 bg-gray-800 border-gray-700 rounded-lg py-2 text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Link
|
||||
to="/product/new"
|
||||
className="flex items-center bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition"
|
||||
>
|
||||
<Plus className="h-5 w-5 mr-1" />
|
||||
Add Product
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500 text-red-200 p-4 rounded-lg mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-20 text-gray-400">Loading products...</div>
|
||||
) : products.length === 0 ? (
|
||||
<div className="text-center py-20 text-gray-400">No products found.</div>
|
||||
) : (
|
||||
<>
|
||||
{viewMode === 'table' ? (
|
||||
<div className="bg-gray-800 rounded-xl shadow-xl overflow-hidden border border-gray-700">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-700">
|
||||
<thead className="bg-gray-750">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Product</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Price</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Stock</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{products.map((product) => (
|
||||
<tr key={product.id} className="hover:bg-gray-750 transition">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="h-16 w-16 flex-shrink-0 bg-gray-700 rounded-md overflow-hidden flex items-center justify-center border border-gray-600">
|
||||
{product.images?.[0]?.src ? (
|
||||
<img className="h-full w-full object-cover" src={product.images[0].src} alt="" />
|
||||
) : (
|
||||
<ImageIcon className="h-6 w-6 text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-white max-w-xs truncate" title={product.name}>{product.name}</div>
|
||||
<div className="text-sm text-gray-500">ID: {product.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-300">
|
||||
<span dangerouslySetInnerHTML={{ __html: product.price_html || ('$' + product.price) }} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-300">
|
||||
{product.manage_stock ? product.stock_quantity : 'Unlimited'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${product.stock_status === 'instock' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{product.stock_status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link to={`/product/${product.id}`} className="text-blue-400 hover:text-blue-300 mr-4">
|
||||
<Edit className="h-5 w-5 inline" />
|
||||
</Link>
|
||||
<button onClick={() => handleDelete(product.id)} className="text-red-400 hover:text-red-300">
|
||||
<Trash2 className="h-5 w-5 inline" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="bg-gray-800 rounded-xl shadow-lg border border-gray-700 overflow-hidden hover:shadow-xl transition flex flex-col">
|
||||
<div className="h-48 bg-gray-700 overflow-hidden relative">
|
||||
{product.images?.[0]?.src ? (
|
||||
<img className="w-full h-full object-cover" src={product.images[0].src} alt="" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-500">
|
||||
<ImageIcon className="h-10 w-10" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-2 right-2 flex space-x-1">
|
||||
<span className={`px-2 py-1 text-xs font-semibold rounded-full shadow-sm ${product.stock_status === 'instock' ? 'bg-green-100/90 text-green-800' : 'bg-red-100/90 text-red-800'
|
||||
}`}>
|
||||
{product.stock_status === 'instock' ? 'In Stock' : 'Out'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex-1 flex flex-col">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-lg font-bold text-white line-clamp-2" title={product.name}>{product.name}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-4">ID: {product.id}</p>
|
||||
<div className="mt-auto flex items-center justify-between">
|
||||
<span className="text-lg font-semibold text-blue-300" dangerouslySetInnerHTML={{ __html: product.price_html || ('$' + product.price) }} />
|
||||
<div className="flex space-x-3">
|
||||
<Link to={`/product/${product.id}`} className="text-gray-400 hover:text-blue-400 transition">
|
||||
<Edit className="h-5 w-5" />
|
||||
</Link>
|
||||
<button onClick={() => handleDelete(product.id)} className="text-gray-400 hover:text-red-400 transition">
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="flex items-center text-gray-400 hover:text-white disabled:opacity-50"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5 mr-1" />
|
||||
Previous
|
||||
</button>
|
||||
<span className="text-gray-400">Page {page}</span>
|
||||
<button
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
className="flex items-center text-gray-400 hover:text-white"
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-5 w-5 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
background-color: #f3f4f6;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "edada7c56edf4a183c1735310e123c7f923584f1"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
- platform: android
|
||||
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
- platform: ios
|
||||
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
- platform: linux
|
||||
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
- platform: macos
|
||||
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
- platform: web
|
||||
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
- platform: windows
|
||||
create_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
base_revision: edada7c56edf4a183c1735310e123c7f923584f1
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# mobile_app
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.mobile_app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.mobile_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="mobile_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example.mobile_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
if (project.name == "app") {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
WARNING: A restricted method in java.lang.System has been called
|
||||
WARNING: java.lang.System::load has been called by net.rubygrapefruit.platform.internal.NativeLibraryLoader in an unnamed module (file:/C:/Users/mtkel/.gradle/wrapper/dists/gradle-8.12-all/ejduaidbjup3bmmkhw3rie4zb/gradle-8.12/lib/native-platform-0.22-milestone-27.jar)
|
||||
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
|
||||
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
|
||||
|
||||
|
||||
FAILURE: Build failed with an exception.
|
||||
|
||||
* What went wrong:
|
||||
25
|
||||
|
||||
* Try:
|
||||
> Run with --stacktrace option to get the stack trace.
|
||||
> Run with --info or --debug option to get more log output.
|
||||
> Run with --scan to get full insights.
|
||||
> Get more help at https://help.gradle.org.
|
||||
|
||||
BUILD FAILED in 2s
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
WARNING: A restricted method in java.lang.System has been called
|
||||
WARNING: java.lang.System::load has been called by net.rubygrapefruit.platform.internal.NativeLibraryLoader in an unnamed module (file:/C:/Users/mtkel/.gradle/wrapper/dists/gradle-8.12-all/ejduaidbjup3bmmkhw3rie4zb/gradle-8.12/lib/native-platform-0.22-milestone-27.jar)
|
||||
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
|
||||
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
|
||||
|
||||
Initialized native services in: C:\Users\mtkel\.gradle\native
|
||||
Initialized jansi services in: C:\Users\mtkel\.gradle\native
|
||||
Found daemon DaemonInfo{pid=13400, address=[42e99540-8bfb-4471-8279-bf73acaee118 port:59185, addresses:[/127.0.0.1]], state=Idle, lastBusy=1766679695414, context=DefaultDaemonContext[uid=6bf98724-ee1b-4562-b32f-3fa203b52ac2,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=13400,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]} however its context does not match the desired criteria.
|
||||
JVM is incompatible.
|
||||
Wanted: DaemonRequestContext{jvmCriteria=C:\Program Files\OpenJDK\jdk-25 (no JDK specified, using current Java home), daemonOpts=[-XX:MaxMetaspaceSize=4G, -XX:ReservedCodeCacheSize=512m, -XX:+HeapDumpOnOutOfMemoryError, -Xmx8G, -Dfile.encoding=UTF-8, -Duser.country=US, -Duser.language=en, -Duser.variant], applyInstrumentationAgent=true, nativeServicesMode=ENABLED, priority=NORMAL}
|
||||
Actual: DefaultDaemonContext[uid=6bf98724-ee1b-4562-b32f-3fa203b52ac2,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=13400,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]
|
||||
|
||||
Looking for a different daemon...
|
||||
Found daemon DaemonInfo{pid=58204, address=[bbe75fbe-710b-47ab-909d-66a78698b22b port:59247, addresses:[/127.0.0.1]], state=Idle, lastBusy=1766679697302, context=DefaultDaemonContext[uid=a05c09d6-7c75-4e1f-a1df-f745c5df345c,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=58204,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]} however its context does not match the desired criteria.
|
||||
JVM is incompatible.
|
||||
Wanted: DaemonRequestContext{jvmCriteria=C:\Program Files\OpenJDK\jdk-25 (no JDK specified, using current Java home), daemonOpts=[-XX:MaxMetaspaceSize=4G, -XX:ReservedCodeCacheSize=512m, -XX:+HeapDumpOnOutOfMemoryError, -Xmx8G, -Dfile.encoding=UTF-8, -Duser.country=US, -Duser.language=en, -Duser.variant], applyInstrumentationAgent=true, nativeServicesMode=ENABLED, priority=NORMAL}
|
||||
Actual: DefaultDaemonContext[uid=a05c09d6-7c75-4e1f-a1df-f745c5df345c,javaHome=C:\Users\mtkel\.antigravity\extensions\redhat.java-1.50.0-win32-x64\jre\21.0.9-win32-x86_64,javaVersion=21,javaVendor=Eclipse Adoptium,daemonRegistryDir=C:\Users\mtkel\.gradle\daemon,pid=58204,idleTimeout=10800000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:MaxMetaspaceSize=4G,-XX:ReservedCodeCacheSize=512m,-XX:+HeapDumpOnOutOfMemoryError,-Xmx8G,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]
|
||||
|
||||
Looking for a different daemon...
|
||||
The client will now receive all logging from the daemon (pid: 31236). The daemon log file: C:\Users\mtkel\.gradle\daemon\8.12\daemon-31236.out.log
|
||||
Starting 5th build in daemon [uptime: 1 mins 22.562 secs, performance: 100%, GC rate: 0.00/s, heap usage: 0% of 8 GiB, non-heap usage: 1% of 4 GiB]
|
||||
Using 6 worker leases.
|
||||
Now considering [D:\woocommerce_inventory\mobile_app\android] as hierarchies to watch
|
||||
Watching the file system is configured to be enabled if available
|
||||
File system watching is active
|
||||
Starting Build
|
||||
Invalidating in-memory cache of C:\Users\mtkel\.gradle\caches\journal-1\file-access.bin
|
||||
Caching disabled for Kotlin DSL script compilation (Settings/TopLevel/stage1) because:
|
||||
Build cache is disabled
|
||||
|
||||
FAILURE: Build failed with an exception.
|
||||
|
||||
* What went wrong:
|
||||
25
|
||||
|
||||
* Try:
|
||||
> Run with --stacktrace option to get the stack trace.
|
||||
> Run with --debug option to get more log output.
|
||||
> Run with --scan to get full insights.
|
||||
> Get more help at https://help.gradle.org.
|
||||
|
||||
BUILD FAILED in 1s
|
||||
Watched directory hierarchies: []
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.java.home=C:\\Program Files\\Microsoft\\jdk-17.0.17.10-hotspot
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
pluginManagement {
|
||||
val flutterSdkPath = run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.7.3" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -0,0 +1,616 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobileApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
7
mobile_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
BIN
mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
mobile_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
|
|
@ -0,0 +1,5 @@
|
|||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Mobile App</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>mobile_app</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#import "GeneratedPluginRegistrant.h"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ApiService with ChangeNotifier {
|
||||
String? _baseUrl;
|
||||
String? _consumerKey;
|
||||
String? _consumerSecret;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get isLoggedIn => _baseUrl != null && _consumerKey != null && _consumerSecret != null;
|
||||
|
||||
ApiService();
|
||||
|
||||
void connect(String url, String key, String secret) {
|
||||
_baseUrl = url;
|
||||
_consumerKey = key;
|
||||
_consumerSecret = secret;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> verifyCredentials(String url, String key, String secret) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Normalize URL
|
||||
String baseUrl = url.trim();
|
||||
if (!baseUrl.startsWith('http')) baseUrl = 'https://$baseUrl';
|
||||
if (baseUrl.endsWith('/')) baseUrl = baseUrl.substring(0, baseUrl.length - 1);
|
||||
|
||||
// Construct WC API URL
|
||||
final apiUri = Uri.parse('$baseUrl/wp-json/wc/v3/system_status');
|
||||
|
||||
final response = await http.get(
|
||||
apiUri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic ${base64Encode(utf8.encode('$key:$secret'))}',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
String msg = 'Connection failed: ${response.statusCode}';
|
||||
try {
|
||||
final body = jsonDecode(response.body);
|
||||
msg = body['message'] ?? msg;
|
||||
} catch (_) {}
|
||||
throw Exception(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
_error = e.toString().contains('FormatException')
|
||||
? 'Invalid Response. Is this a WooCommerce site?'
|
||||
: e.toString().replaceAll('Exception: ', '');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
_baseUrl = null;
|
||||
_consumerKey = null;
|
||||
_consumerSecret = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
// --- Products ---
|
||||
|
||||
Map<String, String> get _headers => {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic ${base64Encode(utf8.encode('$_consumerKey:$_consumerSecret'))}',
|
||||
};
|
||||
|
||||
// Helper to build WC API URIs
|
||||
Uri _getUri(String path, [Map<String, String>? queryParams]) {
|
||||
// Ensuring path starts with /wp-json/wc/v3
|
||||
// We assume _baseUrl is just the domain e.g. https://site.com
|
||||
// We append the internal implementation path
|
||||
return Uri.parse('$_baseUrl/wp-json/wc/v3$path').replace(queryParameters: queryParams);
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getProducts({int page = 1, String search = ''}) async {
|
||||
if (!isLoggedIn) throw Exception('Not logged in');
|
||||
|
||||
final uri = _getUri('/products', {
|
||||
'page': page.toString(),
|
||||
'per_page': '20',
|
||||
'search': search,
|
||||
});
|
||||
|
||||
final response = await http.get(uri, headers: _headers);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
return jsonDecode(response.body);
|
||||
} on FormatException {
|
||||
throw Exception('Invalid JSON response');
|
||||
}
|
||||
} else {
|
||||
throw Exception('Failed to load products: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getProduct(dynamic id) async {
|
||||
if (!isLoggedIn) throw Exception('Not logged in');
|
||||
|
||||
final uri = _getUri('/products/$id');
|
||||
final response = await http.get(uri, headers: _headers);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to load product');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createProduct(Map<String, dynamic> productData) async {
|
||||
if (!isLoggedIn) throw Exception('Not logged in');
|
||||
|
||||
final uri = _getUri('/products');
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: _headers,
|
||||
body: jsonEncode(productData),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
throw Exception('Failed to create product: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateProduct(int id, Map<String, dynamic> productData) async {
|
||||
if (!isLoggedIn) throw Exception('Not logged in');
|
||||
|
||||
final uri = _getUri('/products/$id');
|
||||
final response = await http.put(
|
||||
uri,
|
||||
headers: _headers,
|
||||
body: jsonEncode(productData),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to update product: ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteProduct(int id) async {
|
||||
if (!isLoggedIn) throw Exception('Not logged in');
|
||||
|
||||
final uri = _getUri('/products/$id', {'force': 'true'});
|
||||
final response = await http.delete(uri, headers: _headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to delete product');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'api_service.dart';
|
||||
import 'profile_manager.dart'; // Import ProfileManager
|
||||
import 'screens/profile_login_screen.dart'; // Change import
|
||||
import 'screens/site_selection_screen.dart'; // New Import
|
||||
import 'screens/product_list_screen.dart';
|
||||
import 'screens/product_editor_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider( // Use MultiProvider
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => ApiService()),
|
||||
ChangeNotifierProvider(create: (_) => ProfileManager()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'Inventory Manager',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.dark,
|
||||
background: const Color(0xFF111827),
|
||||
surface: const Color(0xFF1F2937),
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFF111827),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xFF1F2937),
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF374151),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
hintStyle: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
home: const AuthWrapper(),
|
||||
routes: {
|
||||
'/product/new': (context) => const ProductEditorScreen(),
|
||||
},
|
||||
onGenerateRoute: (settings) {
|
||||
if (settings.name == '/product/edit') {
|
||||
final product = settings.arguments as Map<String, dynamic>;
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => ProductEditorScreen(product: product),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthWrapper extends StatelessWidget {
|
||||
const AuthWrapper({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Current Flow:
|
||||
// 1. App Start
|
||||
// 2. Check ProfileManager.isAuthenticated (PIN Protection)
|
||||
// NO -> ProfileLoginScreen
|
||||
// YES -> SiteSelectionScreen
|
||||
|
||||
return Consumer<ProfileManager>(
|
||||
builder: (context, profile, child) {
|
||||
if (!profile.isAuthenticated) {
|
||||
return const ProfileLoginScreen();
|
||||
} else {
|
||||
return const SiteSelectionScreen();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SiteConfig {
|
||||
final String id;
|
||||
final String name;
|
||||
final String url;
|
||||
final String consumerKey;
|
||||
final String consumerSecret;
|
||||
|
||||
SiteConfig({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.url,
|
||||
required this.consumerKey,
|
||||
required this.consumerSecret,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'consumerKey': consumerKey,
|
||||
'consumerSecret': consumerSecret,
|
||||
};
|
||||
|
||||
factory SiteConfig.fromJson(Map<String, dynamic> json) => SiteConfig(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
url: json['url'],
|
||||
consumerKey: json['consumerKey'],
|
||||
consumerSecret: json['consumerSecret'],
|
||||
);
|
||||
}
|
||||
|
||||
class ProfileManager with ChangeNotifier {
|
||||
bool _isAuthenticated = false;
|
||||
bool _hasPin = false;
|
||||
List<SiteConfig> _sites = [];
|
||||
|
||||
bool get isAuthenticated => _isAuthenticated;
|
||||
bool get hasPin => _hasPin;
|
||||
List<SiteConfig> get sites => _sites;
|
||||
|
||||
ProfileManager() {
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_hasPin = prefs.containsKey('app_pin');
|
||||
|
||||
final sitesJson = prefs.getStringList('saved_sites') ?? [];
|
||||
_sites = sitesJson.map((s) => SiteConfig.fromJson(jsonDecode(s))).toList();
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setPin(String pin) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('app_pin', pin);
|
||||
_hasPin = true;
|
||||
_isAuthenticated = true; // Auto login on creation
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> unlock(String pin) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final storedPin = prefs.getString('app_pin');
|
||||
if (storedPin == pin) {
|
||||
_isAuthenticated = true;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> addSite(SiteConfig site) async {
|
||||
_sites.add(site);
|
||||
await _saveSites();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeSite(String id) async {
|
||||
_sites.removeWhere((s) => s.id == id);
|
||||
await _saveSites();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _saveSites() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final sitesJson = _sites.map((s) => jsonEncode(s.toJson())).toList();
|
||||
await prefs.setStringList('saved_sites', sitesJson);
|
||||
}
|
||||
|
||||
void logout() {
|
||||
_isAuthenticated = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../api_service.dart';
|
||||
import '../profile_manager.dart';
|
||||
|
||||
class AddSiteScreen extends StatefulWidget {
|
||||
const AddSiteScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AddSiteScreen> createState() => _AddSiteScreenState();
|
||||
}
|
||||
|
||||
class _AddSiteScreenState extends State<AddSiteScreen> {
|
||||
final _nameController = TextEditingController();
|
||||
final _urlController = TextEditingController();
|
||||
final _keyController = TextEditingController();
|
||||
final _secretController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
String _getPlatformHint() {
|
||||
if (kIsWeb) return 'e.g. localhost:8000';
|
||||
if (defaultTargetPlatform == TargetPlatform.android) return 'e.g. 10.0.2.2:8000';
|
||||
return 'e.g. localhost:8000';
|
||||
}
|
||||
|
||||
void _handleSave() async {
|
||||
if (_nameController.text.isEmpty ||
|
||||
_urlController.text.isEmpty ||
|
||||
_keyController.text.isEmpty ||
|
||||
_secretController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please fill all fields')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final api = Provider.of<ApiService>(context, listen: false);
|
||||
final profile = Provider.of<ProfileManager>(context, listen: false);
|
||||
|
||||
// Verify Direct Connection
|
||||
await api.verifyCredentials(
|
||||
_urlController.text,
|
||||
_keyController.text,
|
||||
_secretController.text
|
||||
);
|
||||
|
||||
// Normalize Target URL for saving
|
||||
String targetUrl = _urlController.text.trim();
|
||||
if (!targetUrl.startsWith('http')) targetUrl = 'https://$targetUrl';
|
||||
if (targetUrl.endsWith('/')) targetUrl = targetUrl.substring(0, targetUrl.length - 1);
|
||||
|
||||
await profile.addSite(SiteConfig(
|
||||
id: const Uuid().v4(),
|
||||
name: _nameController.text,
|
||||
url: targetUrl,
|
||||
consumerKey: _keyController.text,
|
||||
consumerSecret: _secretController.text,
|
||||
));
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Connection failed: $e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Add Store')),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Store Name',
|
||||
hintText: 'e.g. Shoe Store',
|
||||
prefixIcon: Icon(Icons.store),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _urlController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Store URL',
|
||||
hintText: _getPlatformHint(),
|
||||
prefixIcon: const Icon(Icons.link),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _keyController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Consumer Key',
|
||||
prefixIcon: Icon(Icons.key),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _secretController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Consumer Secret',
|
||||
prefixIcon: Icon(Icons.lock),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleSave,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('Verify & Save'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../api_service.dart';
|
||||
|
||||
class ProductEditorScreen extends StatefulWidget {
|
||||
final Map<String, dynamic>? product;
|
||||
|
||||
const ProductEditorScreen({super.key, this.product});
|
||||
|
||||
@override
|
||||
State<ProductEditorScreen> createState() => _ProductEditorScreenState();
|
||||
}
|
||||
|
||||
class _ProductEditorScreenState extends State<ProductEditorScreen> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isSaving = false;
|
||||
|
||||
// Controllers
|
||||
final _nameController = TextEditingController();
|
||||
final _priceController = TextEditingController();
|
||||
final _skuController = TextEditingController();
|
||||
final _descController = TextEditingController();
|
||||
final _shortDescController = TextEditingController();
|
||||
final _stockController = TextEditingController();
|
||||
|
||||
bool _manageStock = false;
|
||||
String _stockStatus = 'instock';
|
||||
List<dynamic> _metaData = [];
|
||||
|
||||
bool get _isNew => widget.product == null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_initializeData();
|
||||
}
|
||||
|
||||
void _initializeData() {
|
||||
if (widget.product != null) {
|
||||
final p = widget.product!;
|
||||
_nameController.text = p['name'] ?? '';
|
||||
_priceController.text = p['regular_price'] ?? '';
|
||||
_skuController.text = p['sku'] ?? '';
|
||||
_descController.text = p['description']?.replaceAll(RegExp(r'<[^>]*>'), '') ?? ''; // Strip HTML for now
|
||||
_shortDescController.text = p['short_description']?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
_stockController.text = (p['stock_quantity'] ?? 0).toString();
|
||||
_manageStock = p['manage_stock'] == true;
|
||||
_stockStatus = p['stock_status'] ?? 'instock';
|
||||
_metaData = List.from(p['meta_data'] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_nameController.dispose();
|
||||
_priceController.dispose();
|
||||
_skuController.dispose();
|
||||
_descController.dispose();
|
||||
_shortDescController.dispose();
|
||||
_stockController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveProduct() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isSaving = true);
|
||||
|
||||
final api = Provider.of<ApiService>(context, listen: false);
|
||||
final productData = {
|
||||
'name': _nameController.text,
|
||||
'regular_price': _priceController.text,
|
||||
'sku': _skuController.text,
|
||||
'description': _descController.text,
|
||||
'short_description': _shortDescController.text,
|
||||
'manage_stock': _manageStock,
|
||||
'stock_quantity': int.tryParse(_stockController.text) ?? 0,
|
||||
'stock_status': _stockStatus,
|
||||
'meta_data': _metaData,
|
||||
};
|
||||
|
||||
try {
|
||||
if (_isNew) {
|
||||
await api.createProduct(productData);
|
||||
} else {
|
||||
await api.updateProduct(widget.product!['id'], productData);
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Product saved successfully')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _addMetaField() {
|
||||
setState(() {
|
||||
_metaData.add({'key': '', 'value': ''});
|
||||
});
|
||||
}
|
||||
|
||||
void _removeMetaField(int index) {
|
||||
setState(() {
|
||||
_metaData.removeAt(index);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateMetaField(int index, String key, String value) {
|
||||
setState(() {
|
||||
// We modify the map in place or create new
|
||||
_metaData[index] = {'key': key, 'value': value};
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_isNew ? 'New Product' : 'Edit Product'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: _isSaving ? null : _saveProduct,
|
||||
)
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: 'General'),
|
||||
Tab(text: 'Inventory'),
|
||||
Tab(text: 'Plugins'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// General Tab
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(labelText: 'Product Name'),
|
||||
validator: (v) => v!.isEmpty ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: TextFormField(
|
||||
controller: _priceController,
|
||||
decoration: const InputDecoration(labelText: 'Price'),
|
||||
keyboardType: TextInputType.number,
|
||||
)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: TextFormField(
|
||||
controller: _skuController,
|
||||
decoration: const InputDecoration(labelText: 'SKU'),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _shortDescController,
|
||||
decoration: const InputDecoration(labelText: 'Short Description'),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _descController,
|
||||
decoration: const InputDecoration(labelText: 'Description'),
|
||||
maxLines: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Inventory Tab
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Manage Stock'),
|
||||
value: _manageStock,
|
||||
onChanged: (v) => setState(() => _manageStock = v),
|
||||
),
|
||||
if (_manageStock)
|
||||
TextFormField(
|
||||
controller: _stockController,
|
||||
decoration: const InputDecoration(labelText: 'Stock Quantity'),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _stockStatus,
|
||||
decoration: const InputDecoration(labelText: 'Stock Status'),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'instock', child: Text('In Stock')),
|
||||
DropdownMenuItem(value: 'outofstock', child: Text('Out of Stock')),
|
||||
DropdownMenuItem(value: 'onbackorder', child: Text('On Backorder')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _stockStatus = v!),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Plugins / Meta Tab
|
||||
ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _metaData.length + 1,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _metaData.length) {
|
||||
return TextButton.icon(
|
||||
onPressed: _addMetaField,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Custom Meta'),
|
||||
);
|
||||
}
|
||||
|
||||
final meta = _metaData[index];
|
||||
final key = meta['key']?.toString() ?? '';
|
||||
final value = meta['value']?.toString() ?? '';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade800),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: const Color(0xFF1F2937),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: TextFormField(
|
||||
initialValue: key,
|
||||
decoration: const InputDecoration(labelText: 'Key', contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 4)),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
onChanged: (v) => _updateMetaField(index, v, value),
|
||||
)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
onPressed: () => _removeMetaField(index),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
initialValue: value,
|
||||
decoration: const InputDecoration(labelText: 'Value', contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8)),
|
||||
maxLines: null,
|
||||
onChanged: (v) => _updateMetaField(index, key, v),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../api_service.dart';
|
||||
|
||||
class ProductListScreen extends StatefulWidget {
|
||||
const ProductListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProductListScreen> createState() => _ProductListScreenState();
|
||||
}
|
||||
|
||||
class _ProductListScreenState extends State<ProductListScreen> {
|
||||
final _scrollController = ScrollController();
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
List<dynamic> _products = [];
|
||||
bool _isLoading = false;
|
||||
int _page = 1;
|
||||
bool _hasMore = true;
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchProducts();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 &&
|
||||
!_isLoading &&
|
||||
_hasMore) {
|
||||
_fetchProducts();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchProducts({bool refresh = false}) async {
|
||||
if (_isLoading) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
if (refresh) {
|
||||
_page = 1;
|
||||
_products.clear();
|
||||
_hasMore = true;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final newProducts = await Provider.of<ApiService>(context, listen: false)
|
||||
.getProducts(page: _page, search: _searchQuery);
|
||||
|
||||
setState(() {
|
||||
_products.addAll(newProducts);
|
||||
_page++;
|
||||
if (newProducts.isEmpty || newProducts.length < 10) { // Assuming page size 10
|
||||
_hasMore = false;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to load products: $e')),
|
||||
);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSearch(String query) {
|
||||
if (_searchQuery == query) return;
|
||||
_searchQuery = query;
|
||||
_fetchProducts(refresh: true);
|
||||
}
|
||||
|
||||
void _deleteProduct(int id) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Product'),
|
||||
content: const Text('Are you sure you want to delete this product?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Delete', style: TextStyle(color: Colors.red))
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
await Provider.of<ApiService>(context, listen: false).deleteProduct(id);
|
||||
setState(() {
|
||||
_products.removeWhere((p) => p['id'] == id);
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Product deleted')),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Inventory'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () => Provider.of<ApiService>(context, listen: false).logout(),
|
||||
)
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onSubmitted: _handleSearch,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search products...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_handleSearch('');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
await Navigator.pushNamed(context, '/product/new');
|
||||
_fetchProducts(refresh: true);
|
||||
},
|
||||
backgroundColor: Colors.blue,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => _fetchProducts(refresh: true),
|
||||
child: _products.isEmpty && !_isLoading
|
||||
? const Center(child: Text('No products found'))
|
||||
: GridView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(8),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 300,
|
||||
childAspectRatio: 0.75,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: _products.length + (_hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == _products.length) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final product = _products[index];
|
||||
final imageUrl = (product['images'] as List).isNotEmpty
|
||||
? product['images'][0]['src']
|
||||
: null;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await Navigator.pushNamed(
|
||||
context,
|
||||
'/product/edit',
|
||||
arguments: product,
|
||||
);
|
||||
_fetchProducts(refresh: true);
|
||||
},
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
color: const Color(0xFF1F2937),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
imageUrl != null
|
||||
? Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
const Center(child: Icon(Icons.broken_image, size: 40)),
|
||||
)
|
||||
: const Center(child: Icon(Icons.image, size: 40, color: Colors.grey)),
|
||||
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: product['stock_status'] == 'instock'
|
||||
? Colors.green.withOpacity(0.9)
|
||||
: Colors.red.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
product['stock_status'] == 'instock' ? 'In Stock' : 'Out',
|
||||
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
product['name'],
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'\$${product['price']}',
|
||||
style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => _deleteProduct(product['id']),
|
||||
child: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../profile_manager.dart';
|
||||
|
||||
class ProfileLoginScreen extends StatefulWidget {
|
||||
const ProfileLoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProfileLoginScreen> createState() => _ProfileLoginScreenState();
|
||||
}
|
||||
|
||||
class _ProfileLoginScreenState extends State<ProfileLoginScreen> {
|
||||
final _pinController = TextEditingController();
|
||||
final _confirmController = TextEditingController();
|
||||
bool _isCreating = false;
|
||||
String _error = '';
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Check if we need to create a PIN
|
||||
final profile = Provider.of<ProfileManager>(context, listen: false);
|
||||
// Note: profile status might update async, so we rely on Consumer in AuthWrapper usually,
|
||||
// but here we are inside the screen.
|
||||
// Actually, AuthWrapper decides if we show this screen.
|
||||
// If we are here, it means !isAuthenticated.
|
||||
// We check hasPin to decide mode.
|
||||
}
|
||||
|
||||
void _handleSubmit() async {
|
||||
final profile = Provider.of<ProfileManager>(context, listen: false);
|
||||
|
||||
if (_pinController.text.length < 4) {
|
||||
setState(() => _error = 'PIN must be at least 4 digits');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile.hasPin) {
|
||||
// Creating Request
|
||||
if (!_isCreating) {
|
||||
// Ask for confirmation
|
||||
setState(() {
|
||||
_isCreating = true;
|
||||
_error = '';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pinController.text != _confirmController.text) {
|
||||
setState(() => _error = 'PINs do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
await profile.setPin(_pinController.text);
|
||||
} else {
|
||||
// Unlocking
|
||||
final success = await profile.unlock(_pinController.text);
|
||||
if (!success) {
|
||||
setState(() => _error = 'Incorrect PIN');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch hasPin to react if it changes (rare here but good practice)
|
||||
final hasPin = Provider.of<ProfileManager>(context).hasPin;
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.lock_outline, size: 64, color: Colors.blue),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
hasPin ? 'Enter Master PIN' : (_isCreating ? 'Confirm Master PIN' : 'Create Master PIN'),
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
hasPin
|
||||
? 'Unlock your secure profiles'
|
||||
: 'Protect your store credentials',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: _pinController,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'PIN',
|
||||
prefixIcon: Icon(Icons.password),
|
||||
),
|
||||
onSubmitted: (_) => _isCreating ? null : _handleSubmit(), // If confirming, focus next or submit
|
||||
),
|
||||
if (_isCreating) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _confirmController,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirm PIN',
|
||||
prefixIcon: Icon(Icons.check),
|
||||
),
|
||||
onSubmitted: (_) => _handleSubmit(),
|
||||
),
|
||||
],
|
||||
if (_error.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(_error, style: const TextStyle(color: Colors.red)),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleSubmit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(hasPin ? 'Unlock' : (_isCreating ? 'Save PIN' : 'Next')),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../profile_manager.dart';
|
||||
import '../api_service.dart';
|
||||
import 'add_site_screen.dart';
|
||||
import 'product_list_screen.dart';
|
||||
|
||||
class SiteSelectionScreen extends StatelessWidget {
|
||||
const SiteSelectionScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profile = Provider.of<ProfileManager>(context);
|
||||
final api = Provider.of<ApiService>(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('My Stores'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.lock),
|
||||
onPressed: () => profile.logout(),
|
||||
tooltip: 'Lock App',
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const AddSiteScreen()));
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: profile.sites.isEmpty
|
||||
? const Center(child: Text('No sites added. Tap + to add one.'))
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: profile.sites.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final site = profile.sites[index];
|
||||
return Card(
|
||||
color: const Color(0xFF1F2937),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.store, color: Colors.blue),
|
||||
),
|
||||
title: Text(site.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(site.url, style: const TextStyle(color: Colors.grey)),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
// Connect API Directly
|
||||
api.connect(site.url, site.consumerKey, site.consumerSecret);
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ProductListWrapper()),
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
// Option to delete
|
||||
showDialog(context: context, builder: (_) => AlertDialog(
|
||||
title: const Text('Delete Site'),
|
||||
content: Text('Remove ${site.name}?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
TextButton(onPressed: () {
|
||||
profile.removeSite(site.id);
|
||||
Navigator.pop(context);
|
||||
}, child: const Text('Delete', style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper to listen to API changes (like logout from within ProductList)
|
||||
|
||||
class ProductListWrapper extends StatelessWidget {
|
||||
const ProductListWrapper({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ApiService>(
|
||||
builder: (context, api, _) {
|
||||
if (!api.isLoggedIn) {
|
||||
// If we logged out (disconnected), pop back to selection
|
||||
// We need to do this carefully during build...
|
||||
// Better: The logout button in ProductList should just api.disconnect() AND Navigator.pop().
|
||||
// So here we likely just show ProductList.
|
||||
return const ProductListScreen();
|
||||
}
|
||||
return const ProductListScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
flutter/ephemeral
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "mobile_app")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.mobile_app")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||