MIF_E31232435/docs/build_requested_mobile_wire...

439 lines
16 KiB
Python

from __future__ import annotations
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import zipfile
ROOT = Path(__file__).resolve().parent
OUT_DIR = ROOT / "wireframe_mobile_requested"
COMBINED = ROOT / "wireframe-mobile-full.png"
ZIP_PATH = ROOT / "wireframe-mobile-screens.zip"
PHONE_W = 430
PHONE_H = 860
CANVAS_W = 520
CANVAS_H = 1010
MARGIN = 32
COLS = 3
ROWS = 5
def font(size: int, bold: bool = False, italic: bool = False) -> ImageFont.FreeTypeFont:
names = []
if bold and italic:
names = ["arialbi.ttf", "Arial Bold Italic.ttf"]
elif bold:
names = ["arialbd.ttf", "Arial Bold.ttf"]
elif italic:
names = ["ariali.ttf", "Arial Italic.ttf"]
else:
names = ["arial.ttf", "Arial.ttf"]
for name in names:
try:
return ImageFont.truetype(name, size)
except OSError:
pass
return ImageFont.load_default()
F = {
"title": font(23, True),
"subtitle": font(16, True),
"body": font(14),
"small": font(12),
"tiny": font(10),
"button": font(14, True),
"label": font(13, True),
}
class Screen:
def __init__(self, title: str):
self.title = title
self.img = Image.new("RGB", (CANVAS_W, CANVAS_H), "white")
self.d = ImageDraw.Draw(self.img)
self.phone_x = (CANVAS_W - PHONE_W) // 2
self.phone_y = 82
self.d.text((CANVAS_W // 2, 36), title, font=F["title"], fill="#111111", anchor="mm")
self.rounded(self.phone_x, self.phone_y, PHONE_W, PHONE_H, 36, "#ffffff", "#777777", 3)
self.rounded(self.phone_x + 155, self.phone_y + 12, 120, 18, 9, "#eeeeee", "#bbbbbb", 1)
self.x0 = self.phone_x + 28
self.y0 = self.phone_y + 54
self.w = PHONE_W - 56
self.bottom_y = self.phone_y + PHONE_H - 86
def rounded(self, x, y, w, h, r=10, fill="#f3f3f3", outline="#9a9a9a", width=2):
self.d.rounded_rectangle([x, y, x + w, y + h], radius=r, fill=fill, outline=outline, width=width)
def rect(self, x, y, w, h, fill="#f3f3f3", outline="#9a9a9a", width=2):
self.d.rectangle([x, y, x + w, y + h], fill=fill, outline=outline, width=width)
def line(self, x1, y1, x2, y2, fill="#9a9a9a", width=2):
self.d.line([x1, y1, x2, y2], fill=fill, width=width)
def text(self, x, y, txt, style="body", fill="#111111", anchor="la", max_width=None):
txt = str(txt)
if max_width is None:
self.d.text((x, y), txt, font=F[style], fill=fill, anchor=anchor)
return
words = txt.split()
lines = []
cur = ""
for word in words:
test = f"{cur} {word}".strip()
if self.d.textlength(test, font=F[style]) <= max_width:
cur = test
else:
if cur:
lines.append(cur)
cur = word
if cur:
lines.append(cur)
for i, line in enumerate(lines[:3]):
self.d.text((x, y + i * 17), line, font=F[style], fill=fill, anchor=anchor)
def header(self, title, sub=None, back=False):
self.rounded(self.phone_x + 1, self.phone_y + 34, PHONE_W - 2, 92, 22, "#dedede", "#999999", 2)
label = f"< {title}" if back else title
self.text(self.x0, self.phone_y + 78, label, "subtitle")
if sub:
self.text(self.x0, self.phone_y + 103, sub, "small", "#555555")
def input(self, x, y, label, placeholder="", w=None, eye=False):
w = w or self.w
self.text(x, y, label, "label")
self.rounded(x, y + 22, w, 44, 10, "#f7f7f7", "#999999", 2)
self.text(x + 14, y + 51, placeholder, "small", "#666666", anchor="lm")
if eye:
self.text(x + w - 25, y + 51, "eye", "tiny", "#555555", anchor="mm")
def button(self, x, y, label, w=None, dark=True):
w = w or self.w
fill = "#111111" if dark else "#f7f7f7"
outline = "#111111" if dark else "#999999"
text = "#ffffff" if dark else "#111111"
self.rounded(x, y, w, 46, 12, fill, outline, 2)
self.text(x + w / 2, y + 24, label, "button", text, anchor="mm")
def card(self, x, y, w, h, title=None, sub=None, right=None):
self.rounded(x, y, w, h, 14, "#f7f7f7", "#9a9a9a", 2)
if title:
self.text(x + 14, y + 18, title, "label", max_width=w - 28)
if sub:
self.text(x + 14, y + 42, sub, "small", "#555555", max_width=w - 28)
if right:
self.text(x + w - 14, y + 24, right, "small", "#111111", anchor="ra")
def bottom_nav(self, active="Home"):
y = self.phone_y + PHONE_H - 62
labels = ["Home", "Produk", "Stok", "Pred", "Lapor"]
item_w = self.w / len(labels)
for i, label in enumerate(labels):
x = self.x0 + i * item_w
if label == active:
self.rounded(x + 3, y, item_w - 6, 38, 8, "#111111", "#111111", 1)
self.text(x + item_w / 2, y + 22, label, "tiny", "#ffffff", anchor="mm")
else:
self.text(x + item_w / 2, y + 22, label, "tiny", "#555555", anchor="mm")
def plus(self, x, y):
self.rounded(x, y, 46, 46, 23, "#111111", "#111111", 2)
self.text(x + 23, y + 25, "+", "title", "#ffffff", anchor="mm")
def save(self, filename: str):
OUT_DIR.mkdir(exist_ok=True)
path = OUT_DIR / filename
self.img.save(path)
return path
def splash():
s = Screen("1. Splash Screen")
x, y = s.x0, s.phone_y + 220
s.rounded(x + 112, y, 150, 150, 28, "#e0e0e0", "#999999", 2)
s.text(x + 187, y + 65, "IKON", "label", "#555555", anchor="mm")
s.text(x + 187, y + 90, "BAHAN KUE", "tiny", "#555555", anchor="mm")
s.text(x + 187, y + 205, "Prediksi Stok", "title", anchor="mm")
s.text(x + 187, y + 235, "Bahan Kue", "title", anchor="mm")
s.text(x + 187, y + 315, "Memuat aplikasi...", "small", "#555555", anchor="mm")
s.text(x + 187, y + 350, ". . .", "subtitle", "#777777", anchor="mm")
return s.save("01_splash_screen.png")
def login():
s = Screen("2. Halaman Login")
x, y = s.x0, s.y0 + 35
s.rounded(x + 137, y, 100, 100, 22, "#dedede", "#999999", 2)
s.text(x + 187, y + 53, "LOGO", "label", "#555555", anchor="mm")
s.text(x + 187, y + 145, "Prediksi Stok Bahan Kue", "subtitle", anchor="mm")
s.input(x, y + 190, "Email", "Masukkan email")
s.input(x, y + 275, "Password", "Masukkan password", eye=True)
s.button(x, y + 380, "Masuk / Login")
s.text(x + s.w, y + 450, "Lupa Password", "small", "#555555", anchor="ra")
s.text(x + 187, y + 500, "Belum punya akun? Daftar", "small", "#111111", anchor="mm")
return s.save("02_login.png")
def register():
s = Screen("3. Halaman Daftar / Register")
s.header("Daftar Akun", "Buat akun baru", back=True)
x, y = s.x0, s.phone_y + 165
s.input(x, y, "Nama Pengguna", "Masukkan nama")
s.input(x, y + 82, "Email", "Masukkan email")
s.input(x, y + 164, "Password", "Masukkan password")
s.input(x, y + 246, "Konfirmasi Password", "Ulangi password")
s.button(x, y + 355, "Daftar")
s.text(x + 187, y + 425, "Sudah punya akun? Login", "small", "#111111", anchor="mm")
return s.save("03_register.png")
def dashboard():
s = Screen("4. Halaman Dashboard")
s.header("Prediksi Stok Bahan Kue", "Tobaku Sulastri")
x, y = s.x0, s.phone_y + 150
s.card(x, y, 176, 76, "Jumlah Bahan", "24 item")
s.card(x + 198, y, 176, 76, "Stok Kritis", "3 bahan")
s.card(x, y + 92, 374, 70, "Prediksi Terbaru", "Tepung: siapkan 12 kg", "RF")
s.text(x, y + 195, "Menu Utama", "label")
menus = [("Produk/Stok", "Bahan"), ("Stok Masuk", "Tambah"), ("Prediksi", "Kebutuhan"), ("Laporan", "Rekap")]
for i, (a, b) in enumerate(menus):
cx = x + (i % 2) * 198
cy = y + 225 + (i // 2) * 98
s.card(cx, cy, 176, 78, a, b)
s.bottom_nav("Home")
return s.save("04_dashboard.png")
def products():
s = Screen("5. Data Produk / Stok Bahan")
s.header("Data Produk / Stok", "Daftar bahan kue", back=True)
x, y = s.x0, s.phone_y + 150
items = [
("Tepung Terigu", "Bahan | Stok 20 kg | Min 5 kg"),
("Gula Pasir", "Bahan | Stok 2 kg | Min 5 kg"),
("Telur", "Bahan | Stok 10 kg | Min 3 kg"),
("Mentega", "Bahan | Stok 4 kg | Min 2 kg"),
]
for i, (name, sub) in enumerate(items):
s.card(x, y + i * 88, s.w, 74, name, sub)
s.text(x + s.w - 70, y + i * 88 + 52, "Edit", "tiny", "#555555")
s.text(x + s.w - 30, y + i * 88 + 52, "Hapus", "tiny", "#555555", anchor="ra")
s.plus(s.phone_x + PHONE_W - 88, s.bottom_y - 16)
s.bottom_nav("Produk")
return s.save("05_data_produk_stok.png")
def product_form():
s = Screen("6. Tambah / Edit Produk")
s.header("Tambah / Edit Produk", "Input data bahan", back=True)
x, y = s.x0, s.phone_y + 150
s.input(x, y, "Nama Bahan", "Contoh: Tepung")
s.input(x, y + 82, "Kategori", "Bahan / Barang")
s.input(x, y + 164, "Jumlah Stok", "0")
s.input(x, y + 246, "Satuan", "kg / pcs")
s.input(x, y + 328, "Stok Minimum", "0")
s.button(x, y + 435, "Simpan")
return s.save("06_tambah_edit_produk.png")
def stock_in():
s = Screen("7. Halaman Stok Masuk")
s.header("Stok Masuk", "Riwayat penambahan stok", back=True)
x, y = s.x0, s.phone_y + 150
rows = [
("Tepung Terigu", "+10 kg | 01/06/2026", "Pembelian"),
("Gula Pasir", "+5 kg | 01/06/2026", "Restock"),
("Mentega", "+2 kg | 31/05/2026", "Supplier"),
("Telur", "+6 kg | 30/05/2026", "Pembelian"),
]
for i, (a, b, c) in enumerate(rows):
s.card(x, y + i * 88, s.w, 74, a, b)
s.text(x + 14, y + i * 88 + 59, c, "tiny", "#777777")
s.plus(s.phone_x + PHONE_W - 88, s.bottom_y - 16)
s.bottom_nav("Stok")
return s.save("07_stok_masuk.png")
def add_stock_in():
s = Screen("8. Tambah Stok Masuk")
s.header("Tambah Stok Masuk", "Stok bertambah otomatis", back=True)
x, y = s.x0, s.phone_y + 150
s.input(x, y, "Pilih Bahan", "Dropdown bahan")
s.input(x, y + 82, "Jumlah Stok Masuk", "0")
s.input(x, y + 164, "Tanggal", "Pilih tanggal")
s.input(x, y + 246, "Keterangan", "Opsional")
s.card(x, y + 335, s.w, 72, "Catatan", "Setelah disimpan, stok bahan bertambah otomatis.")
s.button(x, y + 435, "Simpan")
return s.save("08_tambah_stok_masuk.png")
def prediction():
s = Screen("9. Prediksi Kebutuhan Bahan")
s.header("Prediksi Kebutuhan", "Model utama: Random Forest", back=True)
x, y = s.x0, s.phone_y + 150
s.input(x, y, "Pilih Bahan / Produk", "Dropdown produk")
s.input(x, y + 82, "Periode Prediksi", "Harian / Mingguan / Bulanan")
s.input(x, y + 164, "Parameter Kebutuhan", "Jumlah produksi / input lain")
s.card(x, y + 255, s.w, 76, "Model Prediksi", "Random Forest digunakan sebagai model utama.")
s.button(x, y + 365, "Proses Prediksi")
s.bottom_nav("Pred")
return s.save("09_prediksi_kebutuhan.png")
def prediction_result():
s = Screen("10. Hasil Prediksi")
s.header("Hasil Prediksi", "Rekomendasi stok", back=True)
x, y = s.x0, s.phone_y + 150
s.card(x, y, s.w, 76, "Nama Bahan", "Tepung Terigu")
s.card(x, y + 92, s.w, 78, "Hasil Prediksi", "Permintaan: 12 kg")
s.card(x, y + 184, s.w, 78, "Rekomendasi", "Siapkan stok minimal 15 kg")
s.card(x, y + 276, s.w, 70, "Status Stok", "Aman / Kritis")
s.card(x, y + 360, s.w, 88, "Perbandingan Model", "Random Forest: 92% | Linear Regression: 78%")
s.button(x, y + 475, "Simpan Hasil")
s.button(x, y + 535, "Gunakan untuk Produksi", dark=False)
s.text(x, y + 600, "Jika digunakan, stok bahan berkurang otomatis.", "tiny", "#555555")
return s.save("10_hasil_prediksi.png")
def usage():
s = Screen("11. Penggunaan Bahan")
s.header("Penggunaan Bahan", "Pengurangan stok otomatis", back=True)
x, y = s.x0, s.phone_y + 150
rows = [
("Tepung Terigu", "Terpakai 3 kg | Sisa 17 kg"),
("Gula Pasir", "Terpakai 1 kg | Sisa 4 kg"),
("Telur", "Terpakai 2 kg | Sisa 8 kg"),
("Mentega", "Terpakai 0.5 kg | Sisa 3.5 kg"),
]
for i, (a, b) in enumerate(rows):
s.card(x, y + i * 88, s.w, 74, a, b)
s.text(x + 14, y + i * 88 + 59, "Tanggal: 01/06/2026", "tiny", "#777777")
s.card(x, y + 370, s.w, 64, "Info", "Stok bahan dikurangi otomatis saat produksi.")
s.plus(s.phone_x + PHONE_W - 88, s.bottom_y - 16)
return s.save("11_penggunaan_bahan.png")
def reports():
s = Screen("12. Halaman Laporan")
s.header("Laporan", "Rekap data sistem", back=True)
x, y = s.x0, s.phone_y + 150
cards = ["Laporan Stok Bahan", "Laporan Stok Masuk", "Laporan Penggunaan", "Laporan Hasil Prediksi"]
for i, title in enumerate(cards):
s.card(x, y + i * 76, s.w, 62, title, "Lihat detail")
s.rounded(x, y + 330, s.w, 150, 14, "#f7f7f7", "#999999", 2)
s.text(x + s.w / 2, y + 400, "Placeholder Grafik", "label", "#555555", anchor="mm")
s.line(x + 35, y + 445, x + s.w - 35, y + 355, "#aaaaaa", 2)
s.button(x, y + 515, "Export Laporan")
s.bottom_nav("Lapor")
return s.save("12_laporan.png")
def report_detail():
s = Screen("13. Detail Laporan")
s.header("Detail Laporan", "Data laporan terpilih", back=True)
x, y = s.x0, s.phone_y + 150
s.card(x, y, s.w, 78, "Ringkasan", "Total bahan: 24 | Kritis: 3")
s.text(x, y + 110, "Tabel Data", "label")
s.rounded(x, y + 135, s.w, 260, 12, "#f7f7f7", "#999999", 2)
headers = ["Nama", "Jumlah", "Status"]
for i, h in enumerate(headers):
s.text(x + 18 + i * 118, y + 168, h, "label")
for r in range(4):
yy = y + 205 + r * 42
s.line(x + 12, yy - 18, x + s.w - 12, yy - 18, "#d0d0d0", 1)
s.text(x + 18, yy, f"Data {r+1}", "small", "#555555")
s.text(x + 138, yy, "10 kg", "small", "#555555")
s.text(x + 258, yy, "Aman", "small", "#555555")
s.button(x, y + 440, "Export / Cetak Laporan")
return s.save("13_detail_laporan.png")
def settings():
s = Screen("14. Halaman Pengaturan")
s.header("Pengaturan", "Menu aplikasi", back=True)
x, y = s.x0, s.phone_y + 150
rows = ["Profil Pengguna", "Pengaturan Akun", "Tentang Aplikasi", "Bantuan"]
for i, row in enumerate(rows):
s.card(x, y + i * 82, s.w, 64, row, "Buka menu", ">")
s.button(x, y + 380, "Logout")
s.bottom_nav("Home")
return s.save("14_pengaturan.png")
def profile():
s = Screen("15. Halaman Profil")
s.header("Profil", "Informasi pengguna", back=True)
x, y = s.x0, s.phone_y + 155
s.rounded(x + 137, y, 100, 100, 50, "#dedede", "#999999", 2)
s.text(x + 187, y + 55, "Avatar", "tiny", "#555555", anchor="mm")
s.text(x + 187, y + 145, "Admin Sulastri", "subtitle", anchor="mm")
s.text(x + 187, y + 174, "admin@sulastri.com", "small", "#555555", anchor="mm")
s.card(x, y + 225, s.w, 72, "Nama Pengguna/Admin", "Admin Sulastri")
s.card(x, y + 313, s.w, 72, "Email", "admin@sulastri.com")
s.button(x, y + 420, "Edit Profil")
s.button(x, y + 480, "Logout", dark=False)
return s.save("15_profil.png")
BUILDERS = [
splash,
login,
register,
dashboard,
products,
product_form,
stock_in,
add_stock_in,
prediction,
prediction_result,
usage,
reports,
report_detail,
settings,
profile,
]
def combine(paths: list[Path]):
sheet_w = COLS * CANVAS_W + (COLS + 1) * MARGIN
sheet_h = ROWS * CANVAS_H + (ROWS + 1) * MARGIN + 72
sheet = Image.new("RGB", (sheet_w, sheet_h), "#f2f2f2")
draw = ImageDraw.Draw(sheet)
draw.text((sheet_w // 2, 45), "Wireframe Mobile Aplikasi Prediksi Stok Bahan Kue", font=font(30, True), fill="#111111", anchor="mm")
for idx, path in enumerate(paths):
img = Image.open(path)
row = idx // COLS
col = idx % COLS
x = MARGIN + col * (CANVAS_W + MARGIN)
y = 84 + MARGIN + row * (CANVAS_H + MARGIN)
draw.rounded_rectangle([x - 8, y - 8, x + CANVAS_W + 8, y + CANVAS_H + 8], radius=18, fill="#ffffff", outline="#d0d0d0", width=2)
sheet.paste(img, (x, y))
sheet.save(COMBINED)
def zip_outputs(paths: list[Path]):
if ZIP_PATH.exists():
ZIP_PATH.unlink()
with zipfile.ZipFile(ZIP_PATH, "w", zipfile.ZIP_DEFLATED) as z:
for path in paths:
z.write(path, path.name)
z.write(COMBINED, COMBINED.name)
def main():
OUT_DIR.mkdir(exist_ok=True)
for old in OUT_DIR.glob("*.png"):
old.unlink()
paths = [builder() for builder in BUILDERS]
combine(paths)
zip_outputs(paths)
print(f"Combined wireframe: {COMBINED}")
print(f"Individual screens: {OUT_DIR}")
print(f"ZIP: {ZIP_PATH}")
if __name__ == "__main__":
main()