diff --git a/Android/doric/src/main/java/pub/doric/DoricRegistry.java b/Android/doric/src/main/java/pub/doric/DoricRegistry.java index afc03619..8560ce0f 100644 --- a/Android/doric/src/main/java/pub/doric/DoricRegistry.java +++ b/Android/doric/src/main/java/pub/doric/DoricRegistry.java @@ -17,6 +17,7 @@ package pub.doric; import android.text.TextUtils; +import pub.doric.plugin.NetworkPlugin; import pub.doric.plugin.ShaderPlugin; import pub.doric.shader.HLayoutNode; import pub.doric.shader.ImageNode; @@ -65,6 +66,7 @@ public class DoricRegistry { public DoricRegistry() { this.registerNativePlugin(ShaderPlugin.class); this.registerNativePlugin(ModalPlugin.class); + this.registerNativePlugin(NetworkPlugin.class); this.registerViewNode(RootNode.class); this.registerViewNode(TextNode.class); this.registerViewNode(ImageNode.class); diff --git a/Android/doric/src/main/java/pub/doric/plugin/ModalPlugin.java b/Android/doric/src/main/java/pub/doric/plugin/ModalPlugin.java index f18e86d7..3746964c 100644 --- a/Android/doric/src/main/java/pub/doric/plugin/ModalPlugin.java +++ b/Android/doric/src/main/java/pub/doric/plugin/ModalPlugin.java @@ -17,7 +17,6 @@ package pub.doric.plugin; import android.app.AlertDialog; import android.content.DialogInterface; -import android.text.TextUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; diff --git a/Android/doric/src/main/java/pub/doric/plugin/NetworkPlugin.java b/Android/doric/src/main/java/pub/doric/plugin/NetworkPlugin.java new file mode 100644 index 00000000..004412ce --- /dev/null +++ b/Android/doric/src/main/java/pub/doric/plugin/NetworkPlugin.java @@ -0,0 +1,116 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pub.doric.plugin; + +import android.text.TextUtils; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSONBuilder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.internal.http.HttpMethod; +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; + +/** + * @Description: pub.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-11-21 + */ +@DoricPlugin(name = "network") +public class NetworkPlugin extends DoricJavaPlugin { + private OkHttpClient okHttpClient = new OkHttpClient(); + + public NetworkPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod(name = "request") + public void request(JSDecoder decoder, final DoricPromise promise) { + try { + JSObject requestVal = decoder.decode().asObject(); + String url = requestVal.getProperty("url").asString().value(); + String method = requestVal.getProperty("method").asString().value(); + JSValue headerVal = requestVal.getProperty("headers"); + JSValue dataVal = requestVal.getProperty("data"); + JSValue timeoutVal = requestVal.getProperty("timeout"); + + Headers.Builder headersBuilder = new Headers.Builder(); + if (headerVal.isObject()) { + JSObject headerObject = headerVal.asObject(); + Set headerKeys = headerObject.propertySet(); + for (String key : headerKeys) { + headersBuilder.add(key, headerObject.getProperty(key).asString().value()); + } + } + Headers headers = headersBuilder.build(); + String contentType = headers.get("Content-Type"); + MediaType mediaType = MediaType.parse(TextUtils.isEmpty(contentType) ? "application/json; charset=utf-8" : contentType); + RequestBody requestBody = HttpMethod.permitsRequestBody(method) ? RequestBody.create(mediaType, dataVal.isString() ? dataVal.asString().value() : "") : null; + Request.Builder requestBuilder = new Request.Builder(); + requestBuilder.url(url) + .headers(headers) + .method(method, requestBody); + if (timeoutVal.isNumber() && okHttpClient.connectTimeoutMillis() != timeoutVal.asNumber().toLong()) { + okHttpClient = okHttpClient.newBuilder().connectTimeout(timeoutVal.asNumber().toLong(), TimeUnit.MILLISECONDS).build(); + } + okHttpClient.newCall(requestBuilder.build()).enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + JSONBuilder header = new JSONBuilder(); + for (String key : response.headers().names()) { + header.put(key, response.headers().get(key)); + } + JSONObject jsonObject = new JSONBuilder() + .put("status", response.code()) + .put("headers", header.toJSONObject()) + .put("data", response.body() == null ? "" : response.body().string()) + .toJSONObject(); + promise.resolve(new JavaValue(jsonObject)); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + +} diff --git a/demo/index.ts b/demo/index.ts index dcba746f..51bfdae5 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -8,4 +8,5 @@ export default [ 'src/EffectsDemo', 'src/ImageDemo', 'src/ModalDemo', + 'src/NetworkDemo', ] \ No newline at end of file diff --git a/demo/src/ImageDemo.ts b/demo/src/ImageDemo.ts index efe05436..272ff6a9 100644 --- a/demo/src/ImageDemo.ts +++ b/demo/src/ImageDemo.ts @@ -10,7 +10,7 @@ class ImageDemo extends Panel { layoutConfig: layoutConfig().w(LayoutSpec.AT_MOST), textSize: 30, textColor: Color.WHITE, - bgColor: colors[1], + bgColor: colors[5], textAlignment: gravity().center(), height: 50, }), @@ -29,18 +29,6 @@ class ImageDemo extends Panel { loadCallback: (ret) => { } }), - label('WebP'), - image({ - imageUrl: "https://misc.aotu.io/ONE-SUNDAY/world_cup_2014_42.webp", - loadCallback: (ret) => { - } - }), - label('Lossy WebP'), - image({ - imageUrl: "https://misc.aotu.io/ONE-SUNDAY/world_cup_2014_42_lossy.webp", - loadCallback: (ret) => { - } - }), label('ScaleToFill'), image({ imageUrl, diff --git a/demo/src/NetworkDemo.ts b/demo/src/NetworkDemo.ts new file mode 100644 index 00000000..da218364 --- /dev/null +++ b/demo/src/NetworkDemo.ts @@ -0,0 +1,34 @@ +import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, Text, scroller, layoutConfig, image, IView, IVLayout, ScaleType, modal, IText, network } from "doric"; +import { title, label, colors } from "./utils"; + +@Entry +class NetworkDemo extends Panel { + build(rootView: Group): void { + scroller(vlayout([ + title("Network Demo"), + label('Click me').apply({ + width: 200, + height: 50, + bgColor: colors[0], + textSize: 30, + textColor: Color.WHITE, + layoutConfig: layoutConfig().exactly(), + onClick: () => { + network(context).get('https://m.baidu.com').then( + e => { + modal(context).alert(JSON.stringify(e)) + } + ).catch(e => { + modal(context).toast('Catched:' + JSON.stringify(e)) + }) + } + } as IText), + ]).apply({ + layoutConfig: layoutConfig().atmost().h(LayoutSpec.WRAP_CONTENT), + gravity: gravity().center(), + space: 10, + } as IVLayout)).apply({ + layoutConfig: layoutConfig().atmost(), + }).in(rootView) + } +} \ No newline at end of file diff --git a/demo/src/SliderDemo.ts b/demo/src/SliderDemo.ts index 09cb47db..7ca6da6c 100644 --- a/demo/src/SliderDemo.ts +++ b/demo/src/SliderDemo.ts @@ -1,4 +1,4 @@ -import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, slider, slideItem, image, layoutConfig } from "doric"; +import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, slider, slideItem, image, layoutConfig, ScaleType } from "doric"; import { colors } from "./utils"; const imageUrls = [ @@ -33,7 +33,8 @@ class SliderPanel extends Panel { renderPage: (idx) => { return slideItem(image({ imageUrl: imageUrls[idx % imageUrls.length], - layoutConfig: layoutConfig().w(LayoutSpec.WRAP_CONTENT).h(LayoutSpec.WRAP_CONTENT).a(gravity().center()), + scaleType: ScaleType.ScaleAspectFit, + layoutConfig: layoutConfig().w(LayoutSpec.AT_MOST).h(LayoutSpec.AT_MOST).a(gravity().center()), })).also(it => { let start = idx it.onClick = () => { diff --git a/demo/src/utils.ts b/demo/src/utils.ts index 29f77355..a024952e 100644 --- a/demo/src/utils.ts +++ b/demo/src/utils.ts @@ -1,4 +1,4 @@ -import { Color, text, Stack, Text } from "doric"; +import { Color, text, Stack, Text, layoutConfig, LayoutSpec, gravity } from "doric"; export const colors = [ "#70a1ff", @@ -33,4 +33,16 @@ export function boxStr(str: string, idx = 0) { it.textColor = Color.WHITE it.bgColor = colors[idx || 0] }) +} + +export function title(str: string) { + return text({ + text: "Network Demo", + layoutConfig: layoutConfig().w(LayoutSpec.AT_MOST), + textSize: 30, + textColor: Color.WHITE, + bgColor: colors[1], + textAlignment: gravity().center(), + height: 50, + }) } \ No newline at end of file diff --git a/iOS/Pod/Classes/DoricRegistry.m b/iOS/Pod/Classes/DoricRegistry.m index 44334ace..609eba0c 100644 --- a/iOS/Pod/Classes/DoricRegistry.m +++ b/iOS/Pod/Classes/DoricRegistry.m @@ -22,6 +22,7 @@ #import "DoricRegistry.h" #import "DoricModalPlugin.h" +#import "DoricNetworkPlugin.h" #import "DoricShaderPlugin.h" #import "DoricStackNode.h" #import "DoricVLayoutNode.h" @@ -56,6 +57,7 @@ - (instancetype)init { - (void)innerRegister { [self registerNativePlugin:DoricModalPlugin.class withName:@"modal"]; + [self registerNativePlugin:DoricNetworkPlugin.class withName:@"network"]; [self registerNativePlugin:DoricShaderPlugin.class withName:@"shader"]; [self registerViewNode:DoricStackNode.class withName:@"Stack"]; diff --git a/iOS/Pod/Classes/Plugin/DoricNetworkPlugin.h b/iOS/Pod/Classes/Plugin/DoricNetworkPlugin.h new file mode 100644 index 00000000..3598bd80 --- /dev/null +++ b/iOS/Pod/Classes/Plugin/DoricNetworkPlugin.h @@ -0,0 +1,24 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Created by pengfei.zhou on 2019/11/21. +// + +#import +#import "DoricNativePlugin.h" + +@interface DoricNetworkPlugin : DoricNativePlugin +@end \ No newline at end of file diff --git a/iOS/Pod/Classes/Plugin/DoricNetworkPlugin.m b/iOS/Pod/Classes/Plugin/DoricNetworkPlugin.m new file mode 100644 index 00000000..2d03c35f --- /dev/null +++ b/iOS/Pod/Classes/Plugin/DoricNetworkPlugin.m @@ -0,0 +1,59 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Created by pengfei.zhou on 2019/11/21. +// + +#import "DoricNetworkPlugin.h" + + +@implementation DoricNetworkPlugin +- (void)request:(NSDictionary *)dic withPromise:(DoricPromise *)promise { + NSString *url = dic[@"url"]; + NSString *method = dic[@"method"]; + NSDictionary *headers = dic[@"headers"]; + NSNumber *timeout = dic[@"timeout"]; + NSString *data = dic[@"data"]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; + request.HTTPMethod = method.uppercaseString; + if (timeout) { + request.timeoutInterval = [timeout floatValue] / 1000; + } + if (headers) { + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) { + [request setValue:obj forHTTPHeaderField:key]; + }]; + } + if (data) { + [request setHTTPBody:[data dataUsingEncoding:NSUTF8StringEncoding]]; + } + [[[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]] + dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (!error) { + NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSDictionary *resDic = @{ + @"status": @(((NSHTTPURLResponse *) response).statusCode), + @"headers": ((NSHTTPURLResponse *) response).allHeaderFields, + @"data": dataStr, + }; + [promise resolve:resDic]; + } else { + [promise reject:error.description]; + } + }] resume]; +} +@end diff --git a/js-framework/index.ts b/js-framework/index.ts index e05cc44b..8a5de55b 100644 --- a/js-framework/index.ts +++ b/js-framework/index.ts @@ -28,4 +28,4 @@ export * from './src/util/gravity' export * from './src/util/candies' export * from './src/vm/mvvm' export * from './src/runtime/global' -export * from './src/util/modal' +export * from './src/util/nativeModules' diff --git a/js-framework/src/util/modal.ts b/js-framework/src/util/modal.ts deleted file mode 100644 index ec27dfdd..00000000 --- a/js-framework/src/util/modal.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright [2019] [Doric.Pub] - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { BridgeContext } from "../runtime/global"; -import { Gravity } from "./gravity"; - -export function modal(context: BridgeContext) { - return { - toast: (msg: string, gravity: Gravity = Gravity.Bottom) => { - context.modal.toast({ - msg, - gravity: gravity.toModel(), - }) - }, - alert: (arg: string | { - title: string, - msg: string, - okLabel?: string, - }) => { - if (typeof arg === 'string') { - return context.modal.alert({ msg: arg }) - } else { - return context.modal.alert(arg) - } - }, - confirm: (arg: string | { - title: string, - msg: string, - okLabel?: string, - cancelLabel?: string, - }) => { - if (typeof arg === 'string') { - return context.modal.confirm({ msg: arg }) - } else { - return context.modal.confirm(arg) - } - }, - prompt: (arg: { - title?: string, - msg?: string, - okLabel?: string, - cancelLabel?: string, - text?: string, - defaultText?: string, - }) => { - return context.modal.prompt(arg) - }, - } -} \ No newline at end of file diff --git a/js-framework/src/util/nativeModules.ts b/js-framework/src/util/nativeModules.ts new file mode 100644 index 00000000..3c9c1cde --- /dev/null +++ b/js-framework/src/util/nativeModules.ts @@ -0,0 +1,152 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { BridgeContext } from "../runtime/global"; +import { Gravity } from "./gravity"; + +export function modal(context: BridgeContext) { + return { + toast: (msg: string, gravity: Gravity = Gravity.Bottom) => { + context.modal.toast({ + msg, + gravity: gravity.toModel(), + }) + }, + alert: (arg: string | { + title: string, + msg: string, + okLabel?: string, + }) => { + if (typeof arg === 'string') { + return context.modal.alert({ msg: arg }) + } else { + return context.modal.alert(arg) + } + }, + confirm: (arg: string | { + title: string, + msg: string, + okLabel?: string, + cancelLabel?: string, + }) => { + if (typeof arg === 'string') { + return context.modal.confirm({ msg: arg }) + } else { + return context.modal.confirm(arg) + } + }, + prompt: (arg: { + title?: string, + msg?: string, + okLabel?: string, + cancelLabel?: string, + text?: string, + defaultText?: string, + }) => { + return context.modal.prompt(arg) as Promise + }, + } +} +export interface IRequest { + // `url` is the server URL that will be used for the request + url?: string, + // `method` is the request method to be used when making the request + method?: "get" | "post" | "put" | "delete", + // `headers` are custom headers to be sent + headers?: { [index: string]: string } + // `params` are the URL parameters to be sent with the request + // Must be a plain object or a URLSearchParams object + params?: { [index: string]: string } + // `data` is the data to be sent as the request body + // Only applicable for request methods 'PUT', 'POST', and 'PATCH' + data?: object | string + // `timeout` specifies the number of milliseconds before the request times out. + // If the request takes longer than `timeout`, the request will be aborted. + timeout?: number, // default is `0` (no timeout) +} + +export interface IResponse { + // `data` is the response that was provided by the server + data: any, + // `status` is the HTTP status code from the server response + status: number, + // `headers` the headers that the server responded with + // All header names are lower cased + headers?: { [index: string]: string }, +} + +function transformRequest(request: IRequest) { + let url = request.url || "" + if (request.params !== undefined) { + const queryStrings = [] + for (let key in request.params) { + queryStrings.push(`${key}=${encodeURIComponent(request.params[key])}`) + } + request.url = `${request.url}${url.indexOf('?') >= 0 ? '&' : '?'}${queryStrings.join('&')}` + } + if (typeof request.data === 'object') { + request.data = JSON.stringify(request.data) + } + return request +} +export function network(context: BridgeContext) { + return { + request: (config: IRequest) => { + return context.network.request(transformRequest(config)) as Promise + }, + get: (url: string, config?: IRequest) => { + let finalConfig = config + if (finalConfig === undefined) { + finalConfig = {} + } + finalConfig.url = url + finalConfig.method = "get" + return context.network.request(transformRequest(finalConfig)) as Promise + }, + post: (url: string, data?: object | string, config?: IRequest) => { + let finalConfig = config + if (finalConfig === undefined) { + finalConfig = {} + } + finalConfig.url = url + finalConfig.method = "post" + if (data !== undefined) { + finalConfig.data = data + } + return context.network.request(transformRequest(finalConfig)) as Promise + }, + put: (url: string, data?: object | string, config?: IRequest) => { + let finalConfig = config + if (finalConfig === undefined) { + finalConfig = {} + } + finalConfig.url = url + finalConfig.method = "put" + if (data !== undefined) { + finalConfig.data = data + } + return context.network.request(transformRequest(finalConfig)) as Promise + }, + delete: (url: string, data?: object | string, config?: IRequest) => { + let finalConfig = config + if (finalConfig === undefined) { + finalConfig = {} + } + finalConfig.url = url + finalConfig.method = "delete" + return context.network.request(transformRequest(finalConfig)) as Promise + }, + } +} \ No newline at end of file