This commit is contained in:
Rizky 2023-11-08 14:24:33 +07:00
parent 252e73bdfd
commit a8fe6a691d
8 changed files with 518 additions and 28 deletions

View File

@ -0,0 +1,51 @@
import { ExportMobileConfig } from "../../web/src/render/editor/panel/toolbar/center/mobile/config";
import { exmobile } from "../util/export-mobile";
export const _ = {
url: "/export-mobile/:site_id/:action",
async api(
site_id: string,
action:
| "config"
| "set-config"
| "build-android"
| "remove-android"
| "build-ios"
| "remove-ios",
config?: ExportMobileConfig
) {
if (action === "config") {
return await exmobile.config.read(site_id);
} else if (action === "set-config") {
if (config) {
return await exmobile.config.write(site_id, config);
}
} else if (action === "build-android") {
let result = await exmobile.config.modify(site_id, (conf) => ({
...conf,
android: true,
}));
return result;
} else if (action === "remove-android") {
let result = await exmobile.config.modify(site_id, (conf) => ({
...conf,
android: false,
}));
return result;
} else if (action === "build-ios") {
let result = await exmobile.config.modify(site_id, (conf) => ({
...conf,
ios: true,
}));
return result;
} else if (action === "remove-ios") {
let result = await exmobile.config.modify(site_id, (conf) => ({
...conf,
ios: false,
}));
return result;
}
return "This is export-mobile.ts";
},
};

View File

@ -6,7 +6,6 @@ import { build } from "esbuild";
import { $ } from "execa"; import { $ } from "execa";
import { dirAsync, writeAsync } from "fs-jetpack"; import { dirAsync, writeAsync } from "fs-jetpack";
import { stat } from "fs/promises"; import { stat } from "fs/promises";
import { apiContext } from "service-srv";
import { g } from "utils/global"; import { g } from "utils/global";
import { validate } from "uuid"; import { validate } from "uuid";
import { glb } from "../global"; import { glb } from "../global";

View File

@ -0,0 +1,34 @@
import { dir } from "dir";
import { g } from "utils/global";
import { writeAsync, dirAsync } from "fs-jetpack";
import { ExportMobileConfig } from "../../web/src/render/editor/panel/toolbar/center/mobile/config";
const mpath = (site_id: string, path?: string) =>
dir.path(`${g.datadir}/mobile/${site_id}/${path || ""}`);
export const exmobile = {
config: {
async modify(
site_id: string,
fn: (config: ExportMobileConfig) => ExportMobileConfig
) {
const conf = await this.read(site_id);
let result = fn(conf);
this.write(site_id, result);
return result;
},
write: async (site_id: string, config: ExportMobileConfig) => {
const path = mpath(site_id, "config.json");
await dirAsync(mpath(site_id));
await writeAsync(path, config);
},
read: async (site_id: string) => {
const path = mpath(site_id, "config.json");
if (await Bun.file(path).exists()) {
return await Bun.file(path).json();
}
return null;
},
},
};

View File

@ -12,6 +12,7 @@ import { AddElement } from "./AddElement";
import { Export } from "./Export"; import { Export } from "./Export";
import { NPMImport } from "./NPMImport"; import { NPMImport } from "./NPMImport";
import { APIConfig } from "./api/APIConfig"; import { APIConfig } from "./api/APIConfig";
import { ExportMobile } from "./mobile/export-mobile";
export const ToolbarCenter = () => { export const ToolbarCenter = () => {
const p = useGlobal(EditorGlobal, "EDITOR"); const p = useGlobal(EditorGlobal, "EDITOR");
@ -192,17 +193,28 @@ export const ToolbarCenter = () => {
</> </>
), ),
}, },
// {
// content: (
// <>
// <LiveDeploy />
// </>
// ),
// },
]} ]}
/> />
<div className="w-[5px] h-1"></div> <div className="w-[5px] h-1"></div>
<ToolbarBox <ToolbarBox
items={[
{
content: (
<Popover
content={<ExportMobile />}
popoverClassName={cx(
"bg-white shadow-2xl shadow-slate-400 outline-none border border-slate-300"
)}
>
<MobileIcon />
</Popover>
),
},
]}
/>
{/* <ToolbarBox
items={[ items={[
{ {
content: ( content: (
@ -217,11 +229,28 @@ export const ToolbarCenter = () => {
), ),
}, },
]} ]}
/> /> */}
</div> </div>
); );
}; };
const MobileIcon = () => (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 2.5C4 2.22386 4.22386 2 4.5 2H10.5C10.7761 2 11 2.22386 11 2.5V12.5C11 12.7761 10.7761 13 10.5 13H4.5C4.22386 13 4 12.7761 4 12.5V2.5ZM4.5 1C3.67157 1 3 1.67157 3 2.5V12.5C3 13.3284 3.67157 14 4.5 14H10.5C11.3284 14 12 13.3284 12 12.5V2.5C12 1.67157 11.3284 1 10.5 1H4.5ZM6 11.65C5.8067 11.65 5.65 11.8067 5.65 12C5.65 12.1933 5.8067 12.35 6 12.35H9C9.1933 12.35 9.35 12.1933 9.35 12C9.35 11.8067 9.1933 11.65 9 11.65H6Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
);
const JSIcon = () => ( const JSIcon = () => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -0,0 +1,342 @@
import { FC } from "react";
import { useGlobal, useLocal } from "web-utils";
import { EditorGlobal } from "../../../../logic/global";
export type ExportMobileConfig = {
appId: string;
name: string;
url: string;
android: boolean;
ios: boolean;
icon: string;
splash: string;
};
const setting = {
status: "loading" as
| "loading"
| "ready"
| "building-android"
| "building-ios"
| "saving",
config: null as null | ExportMobileConfig,
};
export const ExportMobileSetting: FC<{
onLoad: (config: ExportMobileConfig) => Promise<void>;
}> = () => {
const p = useGlobal(EditorGlobal, "EDITOR");
const local = useLocal({}, async () => {
setting.config = await api.export_mobile(p.site.id, "config");
local.render();
});
return (
<div className="flex flex-col select-none cursor-default border-b">
<div className="flex items-stretch justify-between">
<div className="p-2 flex-1 flex justify-between">
<div>Export Mobile</div>
</div>
<div className="flex items-stretch">
<div className="flex items-center border-l p-1 px-2 space-x-1">
<div
className={cx(
setting.config?.android ? "text-green-800" : "text-slate-500",
setting.config?.android && "cursor-pointer"
)}
onClick={async () => {
if (setting.config?.android && confirm("Remove Android?")) {
setting.status = "building-android";
local.render();
setting.config = await api.export_mobile(
p.site.id,
"remove-android"
);
setting.status = "ready";
local.render();
}
}}
>
{setting.config?.android ? <Check /> : <Uncheck />}
</div>
<div>Android</div>
{setting.status !== "building-android" ? (
<div
className="hover:opacity-50 bg-green-500 text-white px-2 text-sm cursor-pointer"
onClick={async () => {
setting.status = "building-android";
local.render();
setting.config = await api.export_mobile(
p.site.id,
"build-android"
);
setting.status = "ready";
local.render();
}}
>
{!setting.config?.android ? "Add" : "Rebuild"}
</div>
) : (
<div>...</div>
)}
</div>
<div className="flex border-l items-center p-1 pr-2 space-x-1 ">
<div
className={cx(
setting.config?.ios ? "text-blue-800" : "text-slate-500",
setting.config?.ios && "cursor-pointer"
)}
onClick={async () => {
if (setting.config?.ios && confirm("Remove IOS?")) {
setting.status = "building-ios";
local.render();
setting.config = await api.export_mobile(
p.site.id,
"remove-ios"
);
setting.status = "ready";
local.render();
}
}}
>
{setting.config?.ios ? <Check /> : <Uncheck />}
</div>
<div>IOS</div>
{setting.status !== "building-ios" ? (
<div
className="hover:opacity-50 bg-blue-500 text-white px-2 text-sm cursor-pointer"
onClick={async () => {
setting.status = "building-ios";
local.render();
setting.config = await api.export_mobile(
p.site.id,
"build-ios"
);
setting.status = "ready";
local.render();
}}
>
{!setting.config?.ios ? "Add" : "Rebuild"}
</div>
) : (
<div>...</div>
)}
</div>
</div>
</div>
{setting.config && (
<>
<div className="flex flex-row justify-between">
<div
className={cx(
"flex flex-col flex-1",
css`
input {
outline: none;
padding: 2px 5px;
}
`
)}
>
<Input
render={local.render}
title={"App ID"}
name={"appId"}
placeholder="com.app.name"
site_id={p.site.id}
/>
<Input
render={local.render}
title={"App Name"}
name={"name"}
placeholder="Your App Name"
site_id={p.site.id}
/>
<Input
render={local.render}
title={"App URL"}
name={"url"}
placeholder="https://"
site_id={p.site.id}
/>
</div>
<div className="flex items-center justify-center p-2 border-l border-t">
{setting.status === "saving" ? (
"Saving..."
) : (
<div
onClick={async () => {
setting.status = "saving";
local.render();
await api.export_mobile(
p.site.id,
"set-config",
setting.config
);
if (setting.config?.android) {
setting.status = "building-android";
local.render();
await api.export_mobile(p.site.id, "build-android");
}
if (setting.config?.ios) {
setting.status = "building-ios";
local.render();
await api.export_mobile(p.site.id, "build-ios");
}
setting.status = "ready";
local.render();
}}
className="border border-blue-500 text-blue-500 px-2 cursor-pointer"
>
Save Setting
</div>
)}
</div>
</div>
{(setting.config?.android || setting.config?.ios) && (
<div className="flex items-center justify-end p-2 border-t">
<div className="flex flex-1 items-center space-x-2">
<Img
name="icon"
render={local.render}
site_id={p.site.id}
text="1024px × 1024px"
/>
<Img
name="splash"
render={local.render}
site_id={p.site.id}
text="2732px × 2732px"
/>
</div>
<div className="bg-blue-500 text-white hover:opacity-50 px-2 text-sm cursor-pointer flex space-x-1 py-1 items-center">
<span>Download Project</span>
<span
dangerouslySetInnerHTML={{
__html: `<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.50005 1.04999C7.74858 1.04999 7.95005 1.25146 7.95005 1.49999V8.41359L10.1819 6.18179C10.3576 6.00605 10.6425 6.00605 10.8182 6.18179C10.994 6.35753 10.994 6.64245 10.8182 6.81819L7.81825 9.81819C7.64251 9.99392 7.35759 9.99392 7.18185 9.81819L4.18185 6.81819C4.00611 6.64245 4.00611 6.35753 4.18185 6.18179C4.35759 6.00605 4.64251 6.00605 4.81825 6.18179L7.05005 8.41359V1.49999C7.05005 1.25146 7.25152 1.04999 7.50005 1.04999ZM2.5 10C2.77614 10 3 10.2239 3 10.5V12C3 12.5539 3.44565 13 3.99635 13H11.0012C11.5529 13 12 12.5528 12 12V10.5C12 10.2239 12.2239 10 12.5 10C12.7761 10 13 10.2239 13 10.5V12C13 13.1041 12.1062 14 11.0012 14H3.99635C2.89019 14 2 13.103 2 12V10.5C2 10.2239 2.22386 10 2.5 10Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
}}
></span>
</div>
</div>
)}
</>
)}
</div>
);
};
const Input = (opt: {
render: () => void;
title: string;
name: keyof ExportMobileConfig;
placeholder: string;
site_id: string;
}) => {
if (!setting.config) return null;
return (
<div className="flex items-stretch border-t">
<div className="border-r flex items-center px-2 w-[100px]">
{opt.title}
</div>
<input
value={(setting.config[opt.name] || "") as any}
onChange={(e) => {
if (setting.config)
(setting.config as any)[opt.name] = e.currentTarget.value;
opt.render();
}}
spellCheck={false}
className="focus:bg-blue-50 flex-1"
placeholder={opt.placeholder}
/>
</div>
);
};
const Img = (opt: {
name: string;
text: string;
render: () => void;
site_id: string;
}) => {
const src = (setting.config as any)[opt.name];
return (
<div className=" w-[100px] h-[100px] border flex flex-col">
<div className="flex-1 overflow-hidden relative">
{src && <img className="absolute inset-0" src={src} />}
</div>
<div className="h-[40px] overflow-hidden relative text-blue-500 border-t hover:opacity-50">
<div className="absolute pointer-events-none inset-0 cursor-pointer flex items-center justify-center text-center flex-col">
<div>Upload {opt.name}</div>
<div className="text-xs">{opt.text}</div>
</div>
<input
type="file"
onChange={async (e) => {
if (e.currentTarget.files) {
const res = await api._upload(e.currentTarget.files[0]);
(setting.config as any)[opt.name] = res;
setting.status = "saving";
opt.render();
await api.export_mobile(
opt.site_id,
"set-config",
setting.config
);
setting.status = "ready";
opt.render();
}
}}
className={cx(
"opacity-0 cursor-pointer absolute inset-0",
css`
cursor: pointer !important;
font-size: 0;
`
)}
/>
</div>
</div>
);
};
const Check = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
fill="none"
viewBox="0 0 15 15"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M7.5.877a6.623 6.623 0 100 13.246A6.623 6.623 0 007.5.877zM1.827 7.5a5.673 5.673 0 1111.346 0 5.673 5.673 0 01-11.346 0zm8.332-1.962a.5.5 0 00-.818-.576L6.52 8.972 5.357 7.787a.5.5 0 00-.714.7L6.227 10.1a.5.5 0 00.765-.062l3.167-4.5z"
clipRule="evenodd"
></path>
</svg>
);
const Uncheck = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
fill="none"
viewBox="0 0 15 15"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M.877 7.5a6.623 6.623 0 1113.246 0 6.623 6.623 0 01-13.246 0zM7.5 1.827a5.673 5.673 0 100 11.346 5.673 5.673 0 000-11.346z"
clipRule="evenodd"
></path>
</svg>
);

View File

@ -0,0 +1,18 @@
import { useLocal } from "web-utils";
import { ExportMobileConfig, ExportMobileSetting } from "./config";
export const ExportMobile = () => {
const local = useLocal({
config: null as null | ExportMobileConfig,
});
return (
<div className="w-[450px] min-h-[200px] text-sm flex flex-col">
<ExportMobileSetting
onLoad={async (conf) => {
local.config = conf;
local.render();
}}
/>
</div>
);
};

View File

@ -32,7 +32,7 @@ export const initLive = async (p: PG, domain_or_siteid: string) => {
if (p.status === "init") { if (p.status === "init") {
p.status = "loading"; p.status = "loading";
if (w.mobile && w.mobile.send) { if (w.mobile && w.mobile.bind && w.mobile.send) {
w.mobile.bind(p); w.mobile.bind(p);
w.mobile.send({ type: "ready" }); w.mobile.send({ type: "ready" });
} }

View File

@ -10,15 +10,48 @@ type NOTIF_ARG = {
}; };
export const registerMobile = () => { export const registerMobile = () => {
const default_mobile = { const default_mobile = {
send: () => {},
bind: (p: PG) => {},
notif: { notif: {
register: (user_id: string) => {}, register: (user_id: string) => {},
send: (data: NOTIF_ARG) => {}, send: async (data: NOTIF_ARG) => {
const p = getP();
if (p) {
return await p.script.api._notif("send", {
type: "send",
id:
typeof data.user_id === "string"
? data.user_id
: data.user_id.toString(),
body: data.body,
title: data.title,
data: data.data,
});
}
},
onTap: (data: NOTIF_ARG) => {}, onTap: (data: NOTIF_ARG) => {},
onReceive: (data: NOTIF_ARG) => {}, onReceive: (data: NOTIF_ARG) => {},
}, },
}; };
if (window.parent) {
let config = { notif_token: "", p: null as null | PG }; let config = { notif_token: "", p: null as null | PG };
const getP = () => {
const p = config.p;
if (p && p.site && p.site.api_url) {
const api = w.prasiApi[p.site.api_url];
if (
api &&
api.apiEntry &&
api.apiEntry._notif &&
p.script &&
p.script.api
) {
return p;
}
}
};
if (window.parent) {
window.addEventListener("message", async ({ data: raw }) => { window.addEventListener("message", async ({ data: raw }) => {
if (typeof raw === "object" && raw.mobile) { if (typeof raw === "object" && raw.mobile) {
const data = raw as unknown as const data = raw as unknown as
@ -86,22 +119,6 @@ export const registerMobile = () => {
} }
}); });
const getP = () => {
const p = config.p;
if (p && p.site && p.site.api_url) {
const api = w.prasiApi[p.site.api_url];
if (
api &&
api.apiEntry &&
api.apiEntry._notif &&
p.script &&
p.script.api
) {
return p;
}
}
};
const notifObject = { const notifObject = {
send: (msg: { type: "ready" }) => { send: (msg: { type: "ready" }) => {
window.parent.postMessage({ mobile: true, ...msg }, "*"); window.parent.postMessage({ mobile: true, ...msg }, "*");