Prepare Multi-Casual public analytics release
This commit is contained in:
+27
-5
@@ -4,6 +4,18 @@ plugins {
|
||||
id "org.jetbrains.kotlin.plugin.compose"
|
||||
}
|
||||
|
||||
def umengPublicAppKey = providers.gradleProperty("multicaUmengPublicAppKey")
|
||||
.orElse("")
|
||||
.get()
|
||||
def postHogPublicApiKey = providers.gradleProperty("multicaPostHogPublicApiKey")
|
||||
.orElse("")
|
||||
.get()
|
||||
def postHogPublicHost = providers.gradleProperty("multicaPostHogPublicHost")
|
||||
.orElse("https://us.i.posthog.com")
|
||||
.get()
|
||||
def analyticsRoute = providers.gradleProperty("multicaAnalyticsRoute")
|
||||
.orElse("auto")
|
||||
.get()
|
||||
|
||||
android {
|
||||
namespace "ai.multica.app"
|
||||
@@ -11,7 +23,7 @@ android {
|
||||
buildToolsVersion "36.1.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ai.multica.app"
|
||||
applicationId "ai.multicasual.app"
|
||||
minSdk 24
|
||||
targetSdk 36
|
||||
versionCode 1
|
||||
@@ -37,14 +49,21 @@ android {
|
||||
productFlavors {
|
||||
create("public") {
|
||||
dimension "distribution"
|
||||
applicationId "ai.multica.app"
|
||||
resValue "string", "app_name", "Multica"
|
||||
manifestPlaceholders = [usesCleartextTraffic: "false", deepLinkScheme: "multica"]
|
||||
applicationId "ai.multicasual.app"
|
||||
resValue "string", "app_name", "Multi-Casual"
|
||||
manifestPlaceholders = [usesCleartextTraffic: "false", deepLinkScheme: "multi-casual"]
|
||||
buildConfigField "String", "MULTICA_ENVIRONMENT", "\"public\""
|
||||
buildConfigField "String", "MULTICA_API_BASE_URL", "\"https://api.multica.ai\""
|
||||
buildConfigField "String", "MULTICA_WEB_BASE_URL", "\"https://app.multica.ai\""
|
||||
buildConfigField "String", "MULTICA_WS_BASE_URL", "\"wss://api.multica.ai\""
|
||||
buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multica_auth\""
|
||||
buildConfigField "String", "MULTICA_AUTH_PREFS_NAME", "\"multi_casual_auth\""
|
||||
buildConfigField "String", "ANALYTICS_ROUTE", "\"${analyticsRoute}\""
|
||||
buildConfigField "boolean", "UMENG_ENABLED", "true"
|
||||
buildConfigField "String", "UMENG_APP_KEY", "\"${umengPublicAppKey}\""
|
||||
buildConfigField "String", "UMENG_CHANNEL", "\"public\""
|
||||
buildConfigField "boolean", "POSTHOG_ENABLED", "true"
|
||||
buildConfigField "String", "POSTHOG_API_KEY", "\"${postHogPublicApiKey}\""
|
||||
buildConfigField "String", "POSTHOG_HOST", "\"${postHogPublicHost}\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +75,9 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
publicImplementation "com.umeng.umsdk:common:9.9.2"
|
||||
publicImplementation "com.umeng.umsdk:asms:1.8.7.2"
|
||||
publicImplementation "com.posthog:posthog-android:3.47.0"
|
||||
implementation platform("androidx.compose:compose-bom:2024.10.01")
|
||||
implementation "androidx.activity:activity-compose:1.9.3"
|
||||
implementation "androidx.compose.foundation:foundation"
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
-keep class com.umeng.** { *; }
|
||||
-keep class com.posthog.** { *; }
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package ai.multica.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
final class AnalyticsConsentStore {
|
||||
private static final String PREFS_SUFFIX = "_analytics";
|
||||
private static final String CONSENT_DECIDED = "analytics_consent_decided";
|
||||
private static final String CONSENT_GRANTED = "analytics_consent_granted";
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
AnalyticsConsentStore(Context context) {
|
||||
prefs = context.getApplicationContext()
|
||||
.getSharedPreferences(BuildConfig.MULTICA_AUTH_PREFS_NAME + PREFS_SUFFIX, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
boolean hasDecision() {
|
||||
return prefs.getBoolean(CONSENT_DECIDED, false);
|
||||
}
|
||||
|
||||
boolean isGranted() {
|
||||
return prefs.getBoolean(CONSENT_DECIDED, false)
|
||||
&& prefs.getBoolean(CONSENT_GRANTED, false);
|
||||
}
|
||||
|
||||
void grant() {
|
||||
prefs.edit()
|
||||
.putBoolean(CONSENT_DECIDED, true)
|
||||
.putBoolean(CONSENT_GRANTED, true)
|
||||
.apply();
|
||||
}
|
||||
|
||||
void deny() {
|
||||
prefs.edit()
|
||||
.putBoolean(CONSENT_DECIDED, true)
|
||||
.putBoolean(CONSENT_GRANTED, false)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package ai.multica.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public final class AppAnalytics {
|
||||
private static final String TAG = "MultiCasualAnalytics";
|
||||
static final String EVENT_APP_OPEN = "app_open";
|
||||
private static volatile boolean initialized;
|
||||
private static volatile boolean appOpenTracked;
|
||||
private static AnalyticsProvider provider = AnalyticsProvider.NO_OP;
|
||||
|
||||
private AppAnalytics() {}
|
||||
|
||||
public static boolean shouldPromptForConsent(Context context) {
|
||||
return !new AnalyticsConsentStore(context).hasDecision();
|
||||
}
|
||||
|
||||
public static void grantConsentAndInitialize(Context context) {
|
||||
new AnalyticsConsentStore(context).grant();
|
||||
initializeIfAllowed(context);
|
||||
}
|
||||
|
||||
public static void denyConsent(Context context) {
|
||||
new AnalyticsConsentStore(context).deny();
|
||||
provider.optOut();
|
||||
}
|
||||
|
||||
public static void initializeIfAllowed(Context context) {
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (!new AnalyticsConsentStore(appContext).isGranted()) {
|
||||
Log.i(TAG, "Analytics disabled until user grants consent.");
|
||||
return;
|
||||
}
|
||||
if (initialized) {
|
||||
trackAppOpenOnce(appContext);
|
||||
return;
|
||||
}
|
||||
synchronized (AppAnalytics.class) {
|
||||
if (initialized) {
|
||||
trackAppOpenOnce(appContext);
|
||||
return;
|
||||
}
|
||||
provider = createProvider(appContext);
|
||||
initialized = true;
|
||||
}
|
||||
trackAppOpenOnce(appContext);
|
||||
}
|
||||
|
||||
static String selectedRouteForTesting(Context context) {
|
||||
return selectRoute(context);
|
||||
}
|
||||
|
||||
private static AnalyticsProvider createProvider(Context context) {
|
||||
String route = selectRoute(context);
|
||||
Log.i(TAG, "Analytics route=" + route + " environment=" + BuildConfig.MULTICA_ENVIRONMENT);
|
||||
if ("cn".equals(route)) {
|
||||
return UmengAnalyticsProvider.create(context);
|
||||
}
|
||||
if ("global".equals(route)) {
|
||||
return PostHogAnalyticsProvider.create(context);
|
||||
}
|
||||
return AnalyticsProvider.NO_OP;
|
||||
}
|
||||
|
||||
private static String selectRoute(Context context) {
|
||||
String forced = safe(BuildConfig.ANALYTICS_ROUTE).toLowerCase(Locale.US);
|
||||
if ("cn".equals(forced) || "global".equals(forced) || "none".equals(forced)) {
|
||||
return forced;
|
||||
}
|
||||
if (isLikelyMainlandChina(context)) {
|
||||
return "cn";
|
||||
}
|
||||
return "global";
|
||||
}
|
||||
|
||||
private static boolean isLikelyMainlandChina(Context context) {
|
||||
Locale locale = Build.VERSION.SDK_INT >= 24
|
||||
? context.getResources().getConfiguration().getLocales().get(0)
|
||||
: context.getResources().getConfiguration().locale;
|
||||
String country = locale == null ? "" : safe(locale.getCountry()).toUpperCase(Locale.US);
|
||||
if ("CN".equals(country)) {
|
||||
return true;
|
||||
}
|
||||
String language = locale == null ? "" : safe(locale.getLanguage()).toLowerCase(Locale.US);
|
||||
String timeZone = TimeZone.getDefault().getID();
|
||||
return "zh".equals(language)
|
||||
&& ("Asia/Shanghai".equals(timeZone)
|
||||
|| "Asia/Chongqing".equals(timeZone)
|
||||
|| "Asia/Harbin".equals(timeZone)
|
||||
|| "Asia/Urumqi".equals(timeZone));
|
||||
}
|
||||
|
||||
private static void trackAppOpenOnce(Context context) {
|
||||
if (appOpenTracked) {
|
||||
return;
|
||||
}
|
||||
synchronized (AppAnalytics.class) {
|
||||
if (appOpenTracked) {
|
||||
return;
|
||||
}
|
||||
appOpenTracked = true;
|
||||
}
|
||||
provider.trackAppOpen(appOpenProperties(context));
|
||||
}
|
||||
|
||||
private static Map<String, Object> appOpenProperties(Context context) {
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
params.put("app_version", BuildConfig.VERSION_NAME);
|
||||
params.put("build_flavor", BuildConfig.MULTICA_ENVIRONMENT);
|
||||
params.put("distribution_channel", "public");
|
||||
params.put("analytics_route", selectRoute(context));
|
||||
return params;
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
interface AnalyticsProvider {
|
||||
AnalyticsProvider NO_OP = new AnalyticsProvider() {
|
||||
@Override
|
||||
public void trackAppOpen(Map<String, Object> properties) {}
|
||||
|
||||
@Override
|
||||
public void optOut() {}
|
||||
};
|
||||
|
||||
void trackAppOpen(Map<String, Object> properties);
|
||||
|
||||
void optOut();
|
||||
}
|
||||
|
||||
private static final class UmengAnalyticsProvider implements AnalyticsProvider {
|
||||
private final Context context;
|
||||
private final Method onEventObjectMethod;
|
||||
|
||||
private UmengAnalyticsProvider(Context context, Method onEventObjectMethod) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.onEventObjectMethod = onEventObjectMethod;
|
||||
}
|
||||
|
||||
static AnalyticsProvider create(Context context) {
|
||||
if (!BuildConfig.UMENG_ENABLED || TextUtils.isEmpty(safe(BuildConfig.UMENG_APP_KEY))) {
|
||||
Log.w(TAG, "Umeng selected but UMENG_APP_KEY is empty; analytics not started.");
|
||||
return NO_OP;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
try {
|
||||
Class<?> configureClass = Class.forName("com.umeng.commonsdk.UMConfigure");
|
||||
Class<?> mobclickClass = Class.forName("com.umeng.analytics.MobclickAgent");
|
||||
Class<?> pageModeClass = Class.forName("com.umeng.analytics.MobclickAgent$PageMode");
|
||||
Object manualMode = Enum.valueOf((Class<Enum>) pageModeClass.asSubclass(Enum.class), "MANUAL");
|
||||
mobclickClass.getMethod("setPageCollectionMode", pageModeClass).invoke(null, manualMode);
|
||||
mobclickClass.getMethod("setCatchUncaughtExceptions", boolean.class).invoke(null, false);
|
||||
configureClass.getMethod("setLogEnabled", boolean.class).invoke(null, BuildConfig.DEBUG);
|
||||
configureClass.getMethod("setProcessEvent", boolean.class).invoke(null, true);
|
||||
configureClass.getMethod("init", Context.class, String.class, String.class, int.class, String.class)
|
||||
.invoke(null, appContext, BuildConfig.UMENG_APP_KEY, BuildConfig.UMENG_CHANNEL,
|
||||
configureClass.getField("DEVICE_TYPE_PHONE").getInt(null), null);
|
||||
Method onEventObject = mobclickClass.getMethod("onEventObject", Context.class, String.class, Map.class);
|
||||
Log.i(TAG, "Umeng analytics initialized.");
|
||||
return new UmengAnalyticsProvider(appContext, onEventObject);
|
||||
} catch (Throwable error) {
|
||||
Log.w(TAG, "Umeng initialization failed; app launch continues.", error);
|
||||
return NO_OP;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackAppOpen(Map<String, Object> properties) {
|
||||
try {
|
||||
onEventObjectMethod.invoke(null, context, EVENT_APP_OPEN, properties);
|
||||
Log.i(TAG, "Umeng app_open tracked.");
|
||||
} catch (Throwable error) {
|
||||
Log.w(TAG, "Umeng app_open failed; app continues.", error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void optOut() {}
|
||||
}
|
||||
|
||||
private static final class PostHogAnalyticsProvider implements AnalyticsProvider {
|
||||
private final Object postHog;
|
||||
private final Method captureMethod;
|
||||
private final Method optOutMethod;
|
||||
|
||||
private PostHogAnalyticsProvider(Object postHog, Method captureMethod, Method optOutMethod) {
|
||||
this.postHog = postHog;
|
||||
this.captureMethod = captureMethod;
|
||||
this.optOutMethod = optOutMethod;
|
||||
}
|
||||
|
||||
static AnalyticsProvider create(Context context) {
|
||||
if (!BuildConfig.POSTHOG_ENABLED || TextUtils.isEmpty(safe(BuildConfig.POSTHOG_API_KEY))) {
|
||||
Log.w(TAG, "PostHog selected but POSTHOG_API_KEY is empty; analytics not started.");
|
||||
return NO_OP;
|
||||
}
|
||||
try {
|
||||
Class<?> configClass = Class.forName("com.posthog.android.PostHogAndroidConfig");
|
||||
Object config = configClass.getConstructor(String.class, String.class)
|
||||
.newInstance(BuildConfig.POSTHOG_API_KEY, BuildConfig.POSTHOG_HOST);
|
||||
invokeSetter(configClass, config, "setCaptureApplicationLifecycleEvents", false);
|
||||
invokeSetter(configClass, config, "setCaptureScreenViews", false);
|
||||
invokeSetter(configClass, config, "setCaptureDeepLinks", false);
|
||||
invokeSetter(configClass, config, "setSessionReplay", false);
|
||||
invokeSetter(configClass, config, "setPreloadFeatureFlags", false);
|
||||
invokeSetter(configClass, config, "setRemoteConfig", false);
|
||||
invokeSetter(configClass, config, "setSurveys", false);
|
||||
invokeSetter(configClass, config, "setSetDefaultPersonProperties", false);
|
||||
invokeSetter(configClass, config, "setSendFeatureFlagEvent", false);
|
||||
invokeSetter(configClass, config, "setDebug", BuildConfig.DEBUG);
|
||||
|
||||
Class<?> postHogAndroidClass = Class.forName("com.posthog.android.PostHogAndroid");
|
||||
Field companionField = postHogAndroidClass.getField("Companion");
|
||||
Object companion = companionField.get(null);
|
||||
Object shared = companion.getClass()
|
||||
.getMethod("with", Context.class, configClass)
|
||||
.invoke(companion, context.getApplicationContext(), config);
|
||||
Class<?> interfaceClass = Class.forName("com.posthog.PostHogInterface");
|
||||
Method capture = interfaceClass.getMethod(
|
||||
"capture",
|
||||
String.class,
|
||||
String.class,
|
||||
Map.class,
|
||||
Map.class,
|
||||
Map.class,
|
||||
Map.class,
|
||||
Date.class
|
||||
);
|
||||
Method optOut = interfaceClass.getMethod("optOut");
|
||||
Log.i(TAG, "PostHog analytics initialized.");
|
||||
return new PostHogAnalyticsProvider(shared, capture, optOut);
|
||||
} catch (Throwable error) {
|
||||
Log.w(TAG, "PostHog initialization failed; app launch continues.", error);
|
||||
return NO_OP;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackAppOpen(Map<String, Object> properties) {
|
||||
try {
|
||||
captureMethod.invoke(postHog, EVENT_APP_OPEN, null, properties, null, null, null, null);
|
||||
Log.i(TAG, "PostHog app_open tracked.");
|
||||
} catch (Throwable error) {
|
||||
Log.w(TAG, "PostHog app_open failed; app continues.", error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void optOut() {
|
||||
try {
|
||||
optOutMethod.invoke(postHog);
|
||||
} catch (Throwable error) {
|
||||
Log.w(TAG, "PostHog optOut failed; app continues.", error);
|
||||
}
|
||||
}
|
||||
|
||||
private static void invokeSetter(Class<?> configClass, Object config, String methodName, boolean value) {
|
||||
try {
|
||||
configClass.getMethod(methodName, boolean.class).invoke(config, value);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,8 +548,11 @@ private fun ComposePilotApp(
|
||||
initialIssueId: String? = null,
|
||||
initialTabName: String? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var appState by remember { mutableStateOf<PilotState>(PilotState.Loading) }
|
||||
var refresh by remember { mutableIntStateOf(0) }
|
||||
var showAnalyticsConsent by remember { mutableStateOf(AppAnalytics.shouldPromptForConsent(context)) }
|
||||
val zh = authStore.isChinese()
|
||||
|
||||
LaunchedEffect(refresh) {
|
||||
appState = PilotState.Loading
|
||||
@@ -612,6 +615,27 @@ private fun ComposePilotApp(
|
||||
onSessionUpdated = { appState = PilotState.Ready(it) },
|
||||
)
|
||||
}
|
||||
|
||||
MulticaCupertinoAlertDialog(
|
||||
visible = showAnalyticsConsent,
|
||||
title = if (zh) "允许基础使用统计?" else "Allow basic usage analytics?",
|
||||
message = if (zh) {
|
||||
"Multi-Casual 只会上报 app_open 启动事件,用于统计用户量和日活。不会采集页面浏览、聊天内容、文件内容、邮箱、Token 或精确位置。中国大陆用户走友盟+,海外用户走 PostHog。你可以选择不同意,应用仍可继续使用。"
|
||||
} else {
|
||||
"Multi-Casual only reports the app_open event to measure user count and daily active users. It does not collect page views, chat content, file content, email, tokens, or precise location. Mainland China routes to Umeng+, while global users route to PostHog. You can decline and still use the app."
|
||||
},
|
||||
cancelText = if (zh) "不同意" else "Decline",
|
||||
confirmText = if (zh) "同意并继续" else "Agree",
|
||||
contentDescription = "Analytics Consent",
|
||||
onDismissRequest = {
|
||||
AppAnalytics.denyConsent(context)
|
||||
showAnalyticsConsent = false
|
||||
},
|
||||
onConfirm = {
|
||||
AppAnalytics.grantConsentAndInitialize(context)
|
||||
showAnalyticsConsent = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -710,7 +734,7 @@ private fun PilotLoginScreen(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = if (zh) "登录 Multica" else "Sign in to Multica",
|
||||
text = if (zh) "登录 Multi-Casual" else "Sign in to Multi-Casual",
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontSize = 30.sp, lineHeight = 36.sp, fontWeight = FontWeight.Bold),
|
||||
color = MulticaColors.Text,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -983,7 +1007,7 @@ private fun PilotNoWorkspaceOnboarding(
|
||||
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Multica",
|
||||
text = "Multi-Casual",
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontSize = 28.sp, lineHeight = 33.sp),
|
||||
color = MulticaColors.Text,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -2023,7 +2047,7 @@ private fun PilotSearchPage(
|
||||
}
|
||||
|
||||
PilotListPage(
|
||||
title = if (zh) "搜索 Multica" else "Search Multica",
|
||||
title = if (zh) "搜索 Multi-Casual" else "Search Multi-Casual",
|
||||
leading = { PilotSecondaryBackButton(zh = zh, onBack = onBack) },
|
||||
) {
|
||||
item {
|
||||
@@ -3846,7 +3870,7 @@ private fun ChatWindowWebEmptyState(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
MulticaEmptyState(
|
||||
title = if (zh) "欢迎使用 Multica" else "Welcome to Multica",
|
||||
title = if (zh) "欢迎使用 Multi-Casual" else "Welcome to Multi-Casual",
|
||||
description = if (zh) "试试问" else "Try asking",
|
||||
)
|
||||
Column(
|
||||
@@ -13444,11 +13468,11 @@ private fun PilotInlineResultBannersPreview() {
|
||||
.put("id", "comment-preview")
|
||||
.put("content", "Inbox opened this issue at the exact comment.")
|
||||
.put("author_type", "member")
|
||||
.put("author_name", "Multica")
|
||||
.put("author_name", "Multi-Casual")
|
||||
.put("created_at", "2026-05-14T08:00:00Z")
|
||||
)
|
||||
},
|
||||
authorName = "Multica",
|
||||
authorName = "Multi-Casual",
|
||||
expandedActions = false,
|
||||
contentExpanded = true,
|
||||
highlighted = true,
|
||||
@@ -24654,7 +24678,7 @@ private fun feedbackTypeOptions(zh: Boolean): List<FeedbackTypeOption> = listOf(
|
||||
FeedbackTypeOption(
|
||||
key = "feature",
|
||||
label = if (zh) "功能建议" else "Feature request",
|
||||
detail = if (zh) "一个希望 Multica 支持的新能力。" else "A capability you want Multica to support.",
|
||||
detail = if (zh) "一个希望 Multi-Casual 支持的新能力。" else "A capability you want Multi-Casual to support.",
|
||||
icon = Icons.Outlined.Bolt,
|
||||
),
|
||||
FeedbackTypeOption(
|
||||
@@ -26673,9 +26697,9 @@ private fun AgentsWebEmptyState(
|
||||
MulticaEmptyState(
|
||||
title = if (zh) "还没有智能体" else "No agents yet",
|
||||
description = if (zh) {
|
||||
"创建一个智能体,像分配给同事那样把 issue 交给它。本地智能体在你的机器上运行,云智能体在 Multica 运行时上运行。"
|
||||
"创建一个智能体,像分配给同事那样把 issue 交给它。本地智能体在你的机器上运行,云智能体在 Multi-Casual 运行时上运行。"
|
||||
} else {
|
||||
"Create an agent and assign it issues, like any teammate. Local agents run on your machine; cloud agents run on Multica's runtime."
|
||||
"Create an agent and assign it issues, like any teammate. Local agents run on your machine; cloud agents run on Multi-Casual's runtime."
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ public final class MainActivity extends Activity {
|
||||
header.setGravity(Gravity.CENTER_VERTICAL);
|
||||
header.setPadding(dp(18), dp(12), dp(14), dp(10));
|
||||
header.setBackgroundColor(0xFFFFFFFF);
|
||||
TextView title = label("Multica", 22, TEXT, true);
|
||||
TextView title = label("Multi-Casual", 22, TEXT, true);
|
||||
header.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
||||
|
||||
if (!workspaces.isEmpty()) {
|
||||
@@ -2190,7 +2190,7 @@ public final class MainActivity extends Activity {
|
||||
switch (key) {
|
||||
case "loading": return "加载中...";
|
||||
case "sessionExpired": return "登录已过期,请重新登录";
|
||||
case "signIn": return "登录 Multica";
|
||||
case "signIn": return "登录 Multi-Casual";
|
||||
case "signInHint": return "输入邮箱获取登录验证码";
|
||||
case "continue": return "继续";
|
||||
case "google": return "使用 Google 继续";
|
||||
@@ -2303,7 +2303,7 @@ public final class MainActivity extends Activity {
|
||||
switch (key) {
|
||||
case "loading": return "Loading...";
|
||||
case "sessionExpired": return "Session expired. Please sign in again.";
|
||||
case "signIn": return "Sign in to Multica";
|
||||
case "signIn": return "Sign in to Multi-Casual";
|
||||
case "signInHint": return "Enter your email to get a login code";
|
||||
case "continue": return "Continue";
|
||||
case "google": return "Continue with Google";
|
||||
|
||||
@@ -6,5 +6,6 @@ public final class MulticaApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
AppAnalytics.initializeIfAllowed(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ final class OnboardingPayload {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("workspace_id", workspaceId);
|
||||
payload.put("project", new JSONObject()
|
||||
.put("title", "Getting Started with Multica")
|
||||
.put("description", "A lightweight starter project for learning Multica on mobile.")
|
||||
.put("title", "Getting Started with Multi-Casual")
|
||||
.put("description", "A lightweight starter project for learning Multi-Casual on mobile.")
|
||||
.put("icon", "sparkles"));
|
||||
payload.put("welcome_issue_template", new JSONObject()
|
||||
.put("title", "Welcome to Multica")
|
||||
.put("title", "Welcome to Multi-Casual")
|
||||
.put("description", "Use this issue to test comments, Markdown, status changes, and agent collaboration.")
|
||||
.put("priority", "high"));
|
||||
payload.put("agent_guided_sub_issues", new JSONArray()
|
||||
|
||||
@@ -222,7 +222,7 @@ private fun MulticaGlobalActionsBar(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = workspaceName.ifBlank { "Multica" },
|
||||
text = workspaceName.ifBlank { "Multi-Casual" },
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
|
||||
color = MulticaColors.Text,
|
||||
maxLines = 1,
|
||||
@@ -909,14 +909,14 @@ fun MulticaBottomNav(
|
||||
Modifier.hazeEffect(
|
||||
hazeState,
|
||||
style = HazeDefaults.style(
|
||||
backgroundColor = MulticaColors.SurfaceElevated.copy(alpha = 0.72f),
|
||||
backgroundColor = MulticaColors.SurfaceElevated,
|
||||
blurRadius = 28.dp,
|
||||
noiseFactor = 0.07f,
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
.background(MulticaColors.SurfaceElevated.copy(alpha = 0.72f))
|
||||
.background(MulticaColors.SurfaceElevated)
|
||||
.border(0.5.dp, MulticaColors.Border.copy(alpha = 0.62f), RoundedCornerShape(28.dp))
|
||||
.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(3.dp),
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Multica</string>
|
||||
<string name="app_name">Multi-Casual</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user