Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/apps/mobile/harmonyos/entry/build-profile.json5
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@
},
{
"name": "ohosTest",
},
{
// Emulator-only target. The x86_64 emulator cannot install the shipped
// arm64-only `default` artifact, so a separate target carries the ABI
// override. `default` stays arm64-only: nothing else may add x86_64 to it.
// Build with `-p module=entry@emulator`.
"name": "emulator",
"config": {
"buildOption": {
"externalNativeOptions": {
"abiFilters": [
"x86_64"
]
}
}
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default class EntryAbility extends UIAbility {

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
const scenarioId = want.parameters?.['openbitfunDesignPreview'];
if (scenarioId === 'history-scroll' || scenarioId === 'durable-timeline' || scenarioId === 'composer-submit' || scenarioId === 'catalog-refresh' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'device-selector' || scenarioId === 'device-selector-dark' || scenarioId === 'welcome-home' || scenarioId === 'connected-conversation' ||
if (scenarioId === 'history-scroll' || scenarioId === 'durable-timeline' || scenarioId === 'composer-submit' || scenarioId === 'catalog-refresh' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector' || scenarioId === 'model-role-selector-dark' || scenarioId === 'model-role-selector-wide' || scenarioId === 'subagent-process' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-wide' || scenarioId === 'immersive-bottom-drawer' || scenarioId === 'immersive-bottom-settings' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'immersive-bottom-cover' || scenarioId === 'immersive-bottom-miniapps' || scenarioId === 'immersive-bottom-wide-shell' || scenarioId === 'immersive-bottom-tools' || scenarioId === 'immersive-bottom-tools-editor' || scenarioId === 'immersive-bottom-tools-file-action' || scenarioId === 'immersive-bottom-view-settings' || scenarioId === 'immersive-bottom-connect' || scenarioId === 'composer-overflow' || scenarioId === 'composer-overflow-empty' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-wide' || scenarioId === 'composer-overflow-expanded' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector' || scenarioId === 'device-selector-dark' || scenarioId === 'welcome-home' || scenarioId === 'connected-conversation' ||
scenarioId === 'streaming-dark' ||
scenarioId === 'reconnecting-wide' || scenarioId === 'narrow-multiline' ||
scenarioId === 'fold-context' || scenarioId === 'long-reading' ||
Expand All @@ -25,9 +25,9 @@ export default class EntryAbility extends UIAbility {
AppStorage.setOrCreate('scenarioId', scenarioId);
}
try {
const colorMode = scenarioId === 'cold-start-home-dark' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'device-selector-dark' || scenarioId === 'streaming-dark' || scenarioId === 'interaction-mailbox-dark'
const colorMode = scenarioId === 'cold-start-home-dark' || scenarioId === 'catalog-refresh-dark' || scenarioId === 'model-role-selector-dark' || scenarioId === 'subagent-process-dark' || scenarioId === 'file-visibility-dark' || scenarioId === 'immersive-bottom-dark' || scenarioId === 'immersive-bottom-settings-dark' || scenarioId === 'composer-overflow-dark' || scenarioId === 'composer-overflow-expanded-dark' || scenarioId === 'device-selector-dark' || scenarioId === 'streaming-dark' || scenarioId === 'interaction-mailbox-dark'
? ConfigurationConstant.ColorMode.COLOR_MODE_DARK
: scenarioId === 'cold-start-home' || scenarioId === 'catalog-refresh' || scenarioId === 'device-selector' || scenarioId === 'connected-conversation' || scenarioId === 'reconnecting-wide'
: scenarioId === 'cold-start-home' || scenarioId === 'catalog-refresh' || scenarioId === 'model-role-selector' || scenarioId === 'model-role-selector-wide' || scenarioId === 'subagent-process' || scenarioId === 'file-visibility' || scenarioId === 'immersive-bottom' || scenarioId === 'immersive-bottom-wide' || scenarioId === 'immersive-bottom-drawer' || scenarioId === 'immersive-bottom-settings' || scenarioId === 'immersive-bottom-cover' || scenarioId === 'immersive-bottom-miniapps' || scenarioId === 'immersive-bottom-wide-shell' || scenarioId === 'immersive-bottom-tools' || scenarioId === 'immersive-bottom-tools-editor' || scenarioId === 'immersive-bottom-tools-file-action' || scenarioId === 'immersive-bottom-view-settings' || scenarioId === 'immersive-bottom-connect' || scenarioId === 'composer-overflow' || scenarioId === 'composer-overflow-empty' || scenarioId === 'composer-overflow-wide' || scenarioId === 'composer-overflow-expanded' || scenarioId === 'device-selector' || scenarioId === 'connected-conversation' || scenarioId === 'reconnecting-wide'
? ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
: ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
this.context.getApplicationContext().setColorMode(colorMode);
Expand Down Expand Up @@ -63,7 +63,11 @@ export default class EntryAbility extends UIAbility {
try {
const mainWindow = windowStage.getMainWindowSync();
this.mainWindow = mainWindow;
mainWindow.setWindowLayoutFullScreen(false);
// The shell runs immersive: the chat page's transcript has to reach the
// bottom edge of the screen, and pages read the avoid areas themselves to
// keep their content clear of the system bars.
mainWindow.setWindowLayoutFullScreen(true);
this.logWindowInsets(mainWindow);
this.updateSystemBars(this.context.config?.colorMode);
} catch (err) {
hilog.error(DOMAIN, TAG, 'Failed to configure window bars. Cause: %{public}s', JSON.stringify(err));
Expand Down Expand Up @@ -102,6 +106,24 @@ export default class EntryAbility extends UIAbility {
hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onBackground');
}

/**
* The immersive shell is not verifiable from a screenshot alone: whether the
* window covers the status bar and how tall the bottom strip is are device
* facts, so state them once per launch.
*/
private logWindowInsets(mainWindow: window.Window): void {
try {
const system = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const navigation = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
hilog.info(DOMAIN, TAG,
'Window insets statusBar=%{public}s navigation=%{public}s windowRect=%{public}s',
String(system.topRect.height), String(navigation.bottomRect.height),
JSON.stringify(mainWindow.getWindowProperties().windowRect));
} catch (err) {
hilog.error(DOMAIN, TAG, 'Failed to read window insets. Cause: %{public}s', JSON.stringify(err));
}
}

private updateSystemBars(colorMode?: ConfigurationConstant.ColorMode): void {
if (!this.mainWindow) {
return;
Expand All @@ -111,8 +133,12 @@ export default class EntryAbility extends UIAbility {
const content = dark ? MobileDesignColors.ink.dark : MobileDesignColors.ink.light;
try {
this.mainWindow.setWindowSystemBarProperties({
// The status bar keeps the page colour it has always had — the window
// does not draw behind it on every device, and an opaque bar cannot
// regress the header band either way. The navigation bar is transparent
// instead: the chat page's bottom fade is what should be visible there.
statusBarColor: background,
navigationBarColor: background,
navigationBarColor: MobileDesignColors.transparent.light,
statusBarContentColor: content,
navigationBarContentColor: content
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,11 @@ export const EN_US_MESSAGES: [string, string][] = [
['chat.addImage', 'Add image'],
['chat.removeImage', 'Remove image'],
['chat.selectModel', 'Select model'],
['chat.modelPrimary', 'Primary Model'],
['chat.modelFast', 'Fast Model'],
['chat.modelFastNotConfigured', 'Not set, falls back to primary'],
['chat.modelRolePrimary', 'Primary'],
['chat.modelRoleFast', 'Fast'],
['chat.loadOlder', 'Load older messages'],
['chat.loadOlderFailed', 'Could not load earlier messages. Tap to retry.'],
['chat.inputPlaceholder', 'Ask OpenBitFun'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { resourceManager } from '@kit.LocalizationKit';

interface TemplateEntry {
key: string;
template: string;
}

interface StringEntry {
name: string;
value: string;
}

interface StringCatalog {
string: StringEntry[];
}

export class HostTextRetranslator {
private forward: Map<string, string> = new Map();
private reverse: Map<string, string> = new Map();
private templates: TemplateEntry[] = [];

constructor(locale: string) {
this.loadCatalog(locale);
}

private loadCatalog(locale: string): void {
try {
const resourceManager = getContext(this).resourceManager;
const rawResource = resourceManager.getRawFileContentSync('resources/rawfile/string.json');
let jsonString = '';
for (let i = 0; i < rawResource.length; i++) {
jsonString += String.fromCharCode(rawResource[i]);
}
const catalog = JSON.parse(jsonString) as StringCatalog;
const strings = catalog.string || [];

for (let index = 0; index < strings.length; index += 1) {
const entry = strings[index];
const key = entry.name;
const value = entry.value;
this.forward.set(key, value);
this.reverse.set(value, key);
if (value.indexOf('%1$s') >= 0 || value.indexOf('%2$s') >= 0) {
this.templates.push({ key, template: value });
}
}
} catch (error) {
// Fallback: catalog unavailable, retranslate returns original message
}
}

retranslate(message: string): string {
if (message.length === 0) {
return message;
}

// 1. Exact match O(1)
const exactKey = this.reverse.get(message);
if (exactKey) {
return this.forward.get(exactKey) || message;
}

// 2. Template match O(M)
for (let index = 0; index < this.templates.length; index += 1) {
const entry = this.templates[index];
const matched = this.matchTemplate(entry.template, message);
if (matched !== null) {
return this.fillTemplate(entry.key, matched);
}
}

return message;
}

private matchTemplate(template: string, message: string): string | null {
// Parse %1$s and %2$s placeholders
const firstSlot = template.indexOf('%1$s');
if (firstSlot < 0) {
return null;
}

const prefix = template.slice(0, firstSlot);
const afterFirst = template.slice(firstSlot + 4);
const secondSlot = afterFirst.indexOf('%2$s');

if (secondSlot < 0) {
// Single placeholder: %1$s
const suffix = afterFirst;
if (!message.startsWith(prefix) || !message.endsWith(suffix)) {
return null;
}
const value = message.slice(prefix.length, message.length - suffix.length);
return value;
}

// Two placeholders: %1$s and %2$s
const middle = afterFirst.slice(0, secondSlot);
const suffix = afterFirst.slice(secondSlot + 4);
if (!message.startsWith(prefix) || !message.endsWith(suffix)) {
return null;
}
const body = message.slice(prefix.length, message.length - suffix.length);
const middleAt = body.indexOf(middle);
if (middleAt < 0) {
return null;
}
const first = body.slice(0, middleAt);
const second = body.slice(middleAt + middle.length);
return `${first}${second}`;
}

private fillTemplate(key: string, matched: string): string {
const template = this.forward.get(key) || '';
// Replace %1$s and %2$s with the matched value
// For single placeholder, matched is the value
// For two placeholders, matched is concatenated value
return template.replace('%1$s', matched).replace('%2$s', matched);
}
}
Loading
Loading