diff --git a/entry/src/main/ets/components/IconPackManager.ets b/entry/src/main/ets/components/IconPackManager.ets new file mode 100644 index 0000000..a9fce50 --- /dev/null +++ b/entry/src/main/ets/components/IconPackManager.ets @@ -0,0 +1,191 @@ +import { common } from '@kit.AbilityKit'; +import { AegisIconPack, IconPackRegistry } from '../utils/IconPackUtils'; +import { DefaultToastDuration } from '../pages/Base'; +import { parseInstalledIconPacks, refreshInstalledIconPacks } from '../entryability/EntryAbility'; + +@Component +export struct IconPackManager { + @StorageLink('selectedIconPackKey') selectedIconPackKey: string = IconPackRegistry.getDefaultPackKey(); + @StorageLink('installedIconPacksJson') installedIconPacksJson: string = '[]'; + @State expanded: boolean = false; + + aboutToAppear(): void { + this.reloadPacks(); + } + + private getInstalledPacks(): AegisIconPack[] { + return parseInstalledIconPacks(this.installedIconPacksJson); + } + + private async reloadPacks(): Promise { + const context = getContext(this) as common.UIAbilityContext; + const packs = await refreshInstalledIconPacks(context); + if (this.selectedIconPackKey !== IconPackRegistry.getDefaultPackKey() + && !packs.some((pack) => pack.getKey() === this.selectedIconPackKey && !pack.invalid)) { + this.selectedIconPackKey = IconPackRegistry.getDefaultPackKey(); + } + } + + private showToast(message: ResourceStr | string): void { + this.getUIContext().getPromptAction().showToast({ + duration: DefaultToastDuration, + message + }); + } + + private getInstalledCount(): number { + return this.getInstalledPacks().filter((pack) => !pack.invalid).length; + } + + private getPackName(pack: AegisIconPack): ResourceStr | string { + if (pack.invalid) { + return $r('app.string.icon_pack_invalid_name_format', pack.name); + } + return pack.name; + } + + private getPackSubtitle(pack: AegisIconPack): ResourceStr | string { + if (pack.invalid) { + return $r('app.string.icon_pack_invalid_desc'); + } + return $r('app.string.icon_pack_meta_format', pack.icons.length, `${pack.version}`); + } + + private async onImportPressed(): Promise { + const context = getContext(this) as common.UIAbilityContext; + try { + const paths = await IconPackRegistry.pickZipFiles(context); + if (paths.length === 0) { + return; + } + for (const path of paths) { + await IconPackRegistry.importFromZip(context, path); + } + await this.reloadPacks(); + this.showToast($r('app.string.icon_pack_import_success')); + } catch (error) { + this.showToast($r('app.string.icon_pack_import_failed_format', + IconPackRegistry.formatImportError(error as Object))); + } + } + + private async onRemovePressed(pack: AegisIconPack): Promise { + try { + await IconPackRegistry.removePack(pack); + if (this.selectedIconPackKey === pack.getKey()) { + this.selectedIconPackKey = IconPackRegistry.getDefaultPackKey(); + } + await this.reloadPacks(); + this.showToast($r('app.string.icon_pack_remove_success')); + } catch (error) { + this.showToast($r('app.string.icon_pack_remove_failed_format', + IconPackRegistry.formatImportError(error as Object))); + } + } + + @Builder + private packRow(value: string, name: ResourceStr | string, subtitle: ResourceStr | string, selected: boolean, + enabled: boolean, onSelect: () => void, onRemove?: () => void) { + Row() { + Radio({ value, group: 'icon-pack-group' }) + .checked(selected) + .enabled(enabled) + .onChange((checked) => { + if (checked && enabled) { + onSelect(); + } + }) + + Column() { + Text(name) + .fontWeight(FontWeight.Bold) + .textAlign(TextAlign.Start) + Text(subtitle) + .fontSize(12) + .textAlign(TextAlign.Start) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + if (onRemove) { + Button($r('app.string.icon_pack_remove')) + .type(ButtonType.Capsule) + .buttonStyle(ButtonStyleMode.TEXTUAL) + .onClick(() => onRemove()) + } + } + .width('100%') + .padding(12) + .borderRadius(12) + .backgroundColor($r("app.color.icon_pack_row_background")) + } + + build() { + Column() { + Row() { + Text($r('app.string.icon_pack_title')) + .width('85%') + .fontWeight(FontWeight.Bold) + .textAlign(TextAlign.Start) + Button(this.expanded ? $r('app.string.icon_pack_hide') : $r('app.string.icon_pack_manage')) + .type(ButtonType.Capsule) + .buttonStyle(ButtonStyleMode.TEXTUAL) + .onClick(() => { + this.expanded = !this.expanded; + }) + } + .width('100%') + .alignItems(VerticalAlign.Center) + + Row() { + Text($r('app.string.icon_pack_desc')) + .padding({ left: 10 }) + .fontSize(12) + } + .width('100%') + + if (this.expanded) { + Column({ space: 8 }) { + this.packRow( + IconPackRegistry.getDefaultPackKey(), + $r('app.string.icon_pack_builtin'), + $r('app.string.icon_pack_builtin_desc'), + this.selectedIconPackKey === IconPackRegistry.getDefaultPackKey(), + true, + () => { + this.selectedIconPackKey = IconPackRegistry.getDefaultPackKey(); + } + ) + + ForEach(this.getInstalledPacks(), (pack: AegisIconPack) => { + this.packRow( + pack.getKey(), + this.getPackName(pack), + this.getPackSubtitle(pack), + this.selectedIconPackKey === pack.getKey(), + !pack.invalid, + () => { + this.selectedIconPackKey = pack.getKey(); + }, + () => this.onRemovePressed(pack) + ) + }, (pack: AegisIconPack) => pack.getKey()) + + Row({ space: 12 }) { + Button($r('app.string.icon_pack_import')) + .type(ButtonType.Capsule) + .onClick(() => this.onImportPressed()) + Text($r('app.string.icon_pack_installed_count_format', this.getInstalledCount())) + .fontSize(12) + .fontColor('#666666') + } + .margin({ top: 4 }) + } + .width('100%') + .margin({ top: 12, left: 30, right: 20 }) + } + } + .width('100%') + .padding({ top: 8, bottom: 8 }) + } +} diff --git a/entry/src/main/ets/components/TokenIcon.ets b/entry/src/main/ets/components/TokenIcon.ets new file mode 100644 index 0000000..89e15c4 --- /dev/null +++ b/entry/src/main/ets/components/TokenIcon.ets @@ -0,0 +1,86 @@ +import { fileIo as fs } from '@kit.CoreFileKit'; +import { AegisIconPack, IconPackRegistry } from '../utils/IconPackUtils'; +import { parseInstalledIconPacks } from '../entryability/EntryAbility'; + +@Component +export struct TokenIcon { + @Prop issuer: string = ''; + @Prop iconPath: string = ''; + @StorageLink('selectedIconPackKey') selectedIconPackKey: string = IconPackRegistry.getDefaultPackKey(); + @StorageLink('installedIconPacksJson') installedIconPacksJson: string = '[]'; + + private getInstalledPacks(): AegisIconPack[] { + return parseInstalledIconPacks(this.installedIconPacksJson); + } + + private resolveIconPath(): string { + if (this.iconPath && this.isValidPath(this.iconPath)) { + return this.iconPath; + } + return IconPackRegistry.getIconPathByIssuer(this.issuer, this.selectedIconPackKey, this.getInstalledPacks()); + } + + private isValidPath(path: string): boolean { + if (path.startsWith('rawfile://')) { + return true; + } + if (path.startsWith('file://')) { + return fs.accessSync(path.slice(7)); + } + return fs.accessSync(path); + } + + private getImageSource(): ResourceStr | string | undefined { + const path = this.resolveIconPath(); + if (!path) { + return undefined; + } + if (path.startsWith('rawfile://')) { + return $rawfile(path.slice(10)); + } + return path; + } + + private hasImageSource(): boolean { + return this.getImageSource() !== undefined; + } + + private stringToColor(input: string): string { + let hash = 0; + for (let index = 0; index < input.length; index++) { + hash = input.charCodeAt(index) + ((hash << 5) - hash); + } + + let color = '#'; + for (let index = 0; index < 3; index++) { + const value = (hash >> (index * 8)) & 0xff; + const hex = `00${value.toString(16)}`; + color += hex.substring(hex.length - 2); + } + return color; + } + + build() { + Stack() { + if (this.hasImageSource()) { + Image(this.getImageSource()) + .width(40) + .height(40) + .objectFit(ImageFit.Contain) + .draggable(false) + } else { + Text(this.issuer.length > 0 ? this.issuer.charAt(0).toUpperCase() : '?') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + } + } + .width(50) + .height(50) + .padding(5) + .margin(10) + .backgroundColor(this.hasImageSource() ? $r('app.color.vendor_background_color') : + this.stringToColor(this.issuer || 'k')) + .borderRadius(20) + } +} diff --git a/entry/src/main/ets/components/TokenIconPicker.ets b/entry/src/main/ets/components/TokenIconPicker.ets new file mode 100644 index 0000000..bb3584f --- /dev/null +++ b/entry/src/main/ets/components/TokenIconPicker.ets @@ -0,0 +1,266 @@ +import { common } from '@kit.AbilityKit'; +import { PromptParams } from '../pages/Base'; +import { TokenIcon } from './TokenIcon'; +import { IconPackRegistry, IconPackSection } from '../utils/IconPackUtils'; +import { refreshInstalledIconPacks } from '../entryability/EntryAbility'; + +interface TokenIconPickerSectionState { + key: string; + title: string; + isDefault: boolean; +} + +class BasicDataSource implements IDataSource { + private listeners: DataChangeListener[] = []; + + totalCount(): number { + return 0; + } + + getData(_index: number): string { + return ''; + } + + registerDataChangeListener(listener: DataChangeListener): void { + if (this.listeners.indexOf(listener) < 0) { + this.listeners.push(listener); + } + } + + unregisterDataChangeListener(listener: DataChangeListener): void { + const index = this.listeners.indexOf(listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } + } + + notifyDataReload(): void { + this.listeners.forEach((listener) => { + listener.onDataReloaded(); + }); + } +} + +class IconPathDataSource extends BasicDataSource { + private readonly section: IconPackSection; + + constructor(section: IconPackSection) { + super(); + this.section = section; + } + + totalCount(): number { + return this.section.count; + } + + getData(index: number): string { + return this.section.getPathAt(index); + } +} + +const EMPTY_ICON_SECTION: IconPackSection = { + key: 'empty', + title: '', + count: 0, + getPathAt: (_index: number): string => '' +}; + +export class TokenIconPickerData { + issuer: string = ''; + currentIconPath: string = ''; + onConfirm: (path: string) => void = (_path: string): void => { + }; + onClose: () => void = (): void => { + }; +} + +@Component +struct TokenIconPickerContent { + @Prop issuer: string = ''; + @Prop currentIconPath: string = ''; + onConfirm: (path: string) => void = (_path: string): void => { + }; + onClose: () => void = (): void => { + }; + @State suggestedPaths: string[] = []; + @State sectionStates: TokenIconPickerSectionState[] = []; + @State selectedPath: string = ''; + private sectionDataSources: Map = new Map(); + private readonly emptyDataSource: IconPathDataSource = new IconPathDataSource(EMPTY_ICON_SECTION); + + aboutToAppear(): void { + this.selectedPath = this.currentIconPath ?? ''; + const context = getContext(this) as common.UIAbilityContext; + refreshInstalledIconPacks(context).then((packs) => { + const sections = IconPackRegistry.getIconSections(packs); + const nextSources: Map = new Map(); + const nextStates: TokenIconPickerSectionState[] = []; + sections.forEach((section) => { + nextSources.set(section.key, new IconPathDataSource(section)); + nextStates.push({ + key: section.key, + title: section.title, + isDefault: section.key === IconPackRegistry.getDefaultPackKey() + }); + }); + this.sectionDataSources = nextSources; + this.suggestedPaths = IconPackRegistry.getSuggestedIconPaths(this.issuer, packs); + this.sectionStates = nextStates; + }); + } + + private getSectionDataSource(key: string): IconPathDataSource { + return this.sectionDataSources.get(key) ?? this.emptyDataSource; + } + + private closeDialog(): void { + this.onClose(); + } + + private confirmSelection(): void { + this.onConfirm(this.selectedPath); + this.closeDialog(); + } + + private isSelected(path: string): boolean { + return this.selectedPath === path; + } + + @Builder + private sectionTitle(title: ResourceStr | string) { + Text(title) + .fontSize(14) + .fontWeight(FontWeight.Bold) + .margin({ left: 4, top: 8, bottom: 8 }) + .width('100%') + .textAlign(TextAlign.Start) + } + + @Builder + private fixedIconPreview(path: string) { + if (path.startsWith('rawfile://')) { + Image($rawfile(path.slice(10))) + .width(40) + .height(40) + .objectFit(ImageFit.Contain) + .draggable(false) + } else if (path.length > 0) { + Image(path) + .width(40) + .height(40) + .objectFit(ImageFit.Contain) + .draggable(false) + } else { + Text('?') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + } + } + + @Builder + private iconItem(path: string, autoMode: boolean = false) { + ListItem() { + Column({ space: 6 }) { + Stack() { + if (autoMode) { + TokenIcon({ issuer: this.issuer, iconPath: '' }) + } else { + this.fixedIconPreview(path) + } + } + .width(50) + .height(50) + .padding(5) + .margin({ top: 4 }) + .backgroundColor($r('app.color.vendor_background_color')) + .borderRadius(20) + .borderWidth(this.isSelected(path) ? 2 : 0) + .borderColor('#d9485f') + + if (autoMode) { + Text($r('app.string.icon_picker_auto')) + .fontSize(11) + .maxLines(1) + } + } + .width('100%') + .justifyContent(FlexAlign.Center) + .onClick(() => { + this.selectedPath = path; + }) + } + } + + build() { + Column() { + Text($r('app.string.icon_picker_title')) + .fontSize(18) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 8 }) + + List({ space: 8 }) { + ListItemGroup({ header: this.sectionTitle($r('app.string.icon_picker_mode')) }) { + this.iconItem('', true) + } + if (this.suggestedPaths.length > 0) { + ListItemGroup({ header: this.sectionTitle($r('app.string.icon_picker_recommended')) }) { + ForEach(this.suggestedPaths, (path: string) => { + this.iconItem(path) + }, (path: string) => `recommended_${path}`) + } + } + ForEach(this.sectionStates, (section: TokenIconPickerSectionState) => { + ListItemGroup({ + header: this.sectionTitle(section.isDefault ? $r('app.string.icon_pack_builtin') : section.title) + }) { + LazyForEach(this.getSectionDataSource(section.key), (path: string) => { + this.iconItem(path) + }, (path: string) => `${section.key}_${path}`) + } + }, (section: TokenIconPickerSectionState) => section.key) + } + .layoutWeight(1) + .width('100%') + .lanes({ minLength: 72, maxLength: 72 }) + .cachedCount(12) + .scrollBar(BarState.Auto) + + Row() { + Button($r('app.string.common_cancel')) + .layoutWeight(1) + .onClick(() => { + this.closeDialog(); + }) + .margin({right:5}) + Button($r('app.string.common_confirm')) + .layoutWeight(1) + .onClick(() => { + this.confirmSelection(); + }) + .margin({left:5}) + } + .width('100%') + .margin({ top: 8 }) + } + .padding(16) + .width('100%') + .height('90%') + .backgroundColor($r("app.color.icon_picker_background")) + } +} + +@Builder +export function buildTokenIconPicker(params: PromptParams) { + TokenIconPickerContent({ + issuer: params?.data?.issuer ?? '', + currentIconPath: params?.data?.currentIconPath ?? '', + onConfirm: params?.data?.onConfirm ?? ((_path: string): void => { + }), + onClose: params?.data?.onClose ?? (() => { + }) + }) +} + + + diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index acdd00c..2ecfa7a 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -1,68 +1,134 @@ -import { AbilityConstant, bundleManager, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; -import { hilog } from '@kit.PerformanceAnalysisKit'; -import { window } from '@kit.ArkUI'; -import { distributedKVStore } from '@kit.ArkData'; - - -export let kvManager: distributedKVStore.KVManager; - -export default class EntryAbility extends UIAbility { - onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { - hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); - let context = this.context - try { - let bundleName = - bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION).name - kvManager = distributedKVStore.createKVManager({ - context: context, - bundleName: bundleName, - }) - hilog.info(0x0000, 'testTag', '%{public}s', 'Succeeded in creating KVManager'); - } catch (err) { - hilog.error(0x0000, 'testTag', 'Failed to create KVManager. Cause: %{public}s', JSON.stringify(err) ?? ''); - } - } - - onDestroy(): void { - hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); - } - - onWindowStageCreate(windowStage: window.WindowStage): void { - // Main window is created, set main page for this ability - hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); - - windowStage.loadContent('pages/Index', (err) => { - if (err.code) { - hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); - return; - } - hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); - PersistentStorage.persistProp("enableAutoDarkMode", true) - PersistentStorage.persistProp("enableSafeMode", false) - PersistentStorage.persistProp("hideToken", false) - PersistentStorage.persistProp("enhanceNetwork", false) - PersistentStorage.persistProp("developmentMode", false) - PersistentStorage.persistProp("privacyState", "Unknown") - if (AppStorage.get("enableAutoDarkMode")) { - this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); - } else { - this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) - } - }); - } - - onWindowStageDestroy(): void { - // Main window is destroyed, release UI related resources - hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); - } - - onForeground(): void { - // Ability has brought to foreground - hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); - } - - onBackground(): void { - // Ability has back to background - hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); - } -} +import { AbilityConstant, bundleManager, common, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import { distributedKVStore } from '@kit.ArkData'; +import resourceManager from '@ohos.resourceManager'; +import { AegisIconDefinition, AegisIconPack, IconPackRegistry } from '../utils/IconPackUtils'; + +export let kvManager: distributedKVStore.KVManager; + +let appResourceManager: resourceManager.ResourceManager; + +interface StoredIconPack { + invalid: boolean; + onDevicePath: string; + uuid: string; + name: string; + version: number; + icons: AegisIconDefinition[]; +} + +export function getLocalizedString(resource: Resource, ...args: Array): string { + try { + return appResourceManager.getStringSync(resource.id, ...args) + } catch (_) { + return "" + } +} + +export function serializeInstalledIconPacks(packs: AegisIconPack[]): string { + const stored = packs.map((pack): StoredIconPack => ({ + invalid: pack.invalid, + onDevicePath: pack.onDevicePath, + uuid: pack.uuid, + name: pack.name, + version: pack.version, + icons: pack.icons + })) + return JSON.stringify(stored) +} + +export function parseInstalledIconPacks(content: string): AegisIconPack[] { + if (!content) { + return [] + } + + try { + const parsed = JSON.parse(content) as StoredIconPack[] + return parsed.map((item) => { + let pack = new AegisIconPack() + pack.invalid = item.invalid ?? false + pack.onDevicePath = item.onDevicePath ?? '' + pack.uuid = item.uuid ?? '' + pack.name = item.name ?? '' + pack.version = item.version ?? 0 + pack.icons = item.icons ?? [] + pack.buildMap() + return pack + }) + } catch (_) { + return [] + } +} + +export async function refreshInstalledIconPacks(context: common.UIAbilityContext): Promise { + const packs = await IconPackRegistry.loadInstalledPacks(context) + AppStorage.setOrCreate('installedIconPacksJson', serializeInstalledIconPacks(packs)) + return packs +} + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); + let context = this.context + appResourceManager = context.resourceManager + try { + let bundleName = + bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION).name + kvManager = distributedKVStore.createKVManager({ + context: context, + bundleName: bundleName, + }) + hilog.info(0x0000, 'testTag', '%{public}s', 'Succeeded in creating KVManager'); + } catch (err) { + hilog.error(0x0000, 'testTag', 'Failed to create KVManager. Cause: %{public}s', JSON.stringify(err) ?? ''); + } + } + + onDestroy(): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); + PersistentStorage.persistProp("enableAutoDarkMode", true) + PersistentStorage.persistProp("enableSafeMode", false) + PersistentStorage.persistProp("hideToken", false) + PersistentStorage.persistProp("enhanceNetwork", false) + PersistentStorage.persistProp("developmentMode", false) + PersistentStorage.persistProp("privacyState", "Unknown") + PersistentStorage.persistProp("selectedIconPackKey", "default") + PersistentStorage.persistProp("installedIconPacksJson", "[]") + refreshInstalledIconPacks(this.context as common.UIAbilityContext).catch(() => { + }) + if (AppStorage.get("enableAutoDarkMode")) { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + } else { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) + } + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); + } +} diff --git a/entry/src/main/ets/pages/Add.ets b/entry/src/main/ets/pages/Add.ets index 1b38573..318e61c 100644 --- a/entry/src/main/ets/pages/Add.ets +++ b/entry/src/main/ets/pages/Add.ets @@ -1,6 +1,7 @@ -import { AuthData, AuthDataStore, DefaultToastDuration } from "./Base" -import { picker, fileIo as fs } from "@kit.CoreFileKit"; -import { util } from "@kit.ArkTS"; +import { AuthDataStore, DefaultToastDuration } from "./Base"; +import { fileIo as fs, picker } from '@kit.CoreFileKit'; +import { util } from '@kit.ArkTS'; + @Component export struct Add { @@ -15,7 +16,7 @@ export struct Add { Column() { Row() { RelativeContainer() { - Text("通过 TOTP 密钥添加") + Text($r('app.string.add_totp_secret_title')) .fontWeight(FontWeight.Bold) .padding({ left: 10 }) .alignRules({ @@ -40,7 +41,7 @@ export struct Add { }) Row() { - Text("TOTP 密钥是由 A-Z,2-7 组成的字符串,例如:ABCDEFGHIJ234567") + Text($r('app.string.add_totp_secret_desc')) .padding({ left: 20 }) .fontSize(12) } @@ -51,7 +52,7 @@ export struct Add { Column() { Row() { RelativeContainer() { - Text("通过 TOTP URI 添加") + Text($r('app.string.add_totp_uri_title')) .fontWeight(FontWeight.Bold) .padding({ left: 10 }) .alignRules({ @@ -76,7 +77,7 @@ export struct Add { }) Row() { - Text("TOTP URI 是一串以 otpauth://totp/ 开头的链接,例如:otpauth://totp/Example:alice@example.com?secret=ABCDEFGHIJ234567&issuer=Example") + Text($r('app.string.add_totp_uri_desc')) .padding({ left: 20 }) .fontSize(12) } @@ -87,7 +88,7 @@ export struct Add { Column() { Row() { RelativeContainer() { - Text("从令牌密钥文件(*.atsf)恢复") + Text($r('app.string.add_restore_title')) .fontWeight(FontWeight.Bold) .padding({ left: 10 }) .alignRules({ @@ -119,7 +120,7 @@ export struct Add { if (uris.length != 1) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "请选择一个令牌密钥文件" + message: $r('app.string.add_restore_pick_single') }) return } @@ -133,12 +134,12 @@ export struct Add { this.authDataStore.recoverAuthItem(data) this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "恢复完成" + message: $r('app.string.add_restore_success') }) } catch (e) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `恢复错误:${e}` + message: $r('app.string.add_restore_error_format', `${e}`) }) } return @@ -146,7 +147,7 @@ export struct Add { }) Row() { - Text("令牌密钥文件为 *.atsf,可以从当前 App 的设置中导出。对令牌密钥文件做任何更改都可能导致无法恢复。\n注意:恢复操作不会覆盖已添加到 App 的令牌密钥,请自行删除重复的条目") + Text($r('app.string.add_restore_desc')) .padding({ left: 20 }) .fontSize(12) } @@ -157,7 +158,7 @@ export struct Add { Column() { Row() { RelativeContainer() { - Text("添加 Steam 令牌") + Text($r('app.string.add_steam_title')) .fontWeight(FontWeight.Bold) .padding({ left: 10 }) .alignRules({ @@ -186,7 +187,7 @@ export struct Add { }) Row() { - Text("支持 5 位令牌,扫码登陆和交易确认") + Text($r('app.string.add_steam_desc')) .padding({ left: 20 }) .fontSize(12) } @@ -196,7 +197,7 @@ export struct Add { } } } - .title("添加") + .title($r('app.string.add_title')) } } @@ -205,8 +206,7 @@ export struct Add { export struct AddSecret { @Consume("pageStack") pageStack: NavPathStack; @Consume("authDataStore") authDataStore: AuthDataStore; - @State provider: string = "Github" - @State isOtherProvider: boolean = false + @State provider: string = "" @State tokenLength: number = 6 @State account: string = "" @State secret: string = "" @@ -215,75 +215,27 @@ export struct AddSecret { NavDestination() { Column() { Row() { - Text("提供方") + Text($r('app.string.add_secret_provider')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) - Button(this.isOtherProvider ? "其他" : this.provider) + TextInput({ text: this.provider, placeholder: $r('app.string.add_secret_provider_placeholder') }) .width("60%") - .bindMenu([ - { - value: "Github", - action: () => { - this.isOtherProvider = false - this.provider = "Github" - }, - }, - { - value: "Gitlab", - action: () => { - this.isOtherProvider = false - this.provider = "Gitlab" - }, - }, - { - value: "Google", - action: () => { - this.isOtherProvider = false - this.provider = "Google" - }, - }, - { - value: "Microsoft", - action: () => { - this.isOtherProvider = false - this.provider = "Microsoft" - }, - }, - { - value: "其他", - action: () => { - if (!this.isOtherProvider) { - this.provider = "" - } - this.isOtherProvider = true - }, - } - ]) + .fontWeight(FontWeight.Bold) + .textAlign(TextAlign.End) + .maxLength(50) + .onChange((v) => this.provider = v.trim()) } .height(40) .alignItems(VerticalAlign.Center) .margin({ bottom: 10 }) - if (this.isOtherProvider) { - Row() { - TextInput({ text: this.provider, placeholder: "请输入提供方名称" }) - .width("60%") - .fontWeight(FontWeight.Bold) - .textAlign(TextAlign.End) - .maxLength(50) - .onChange((v) => this.provider = v.trim()) - } - .alignSelf(ItemAlign.End) - .margin({ bottom: 10 }) - } - Row() { - Text("账号") + Text($r('app.string.add_secret_account')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) - TextInput({ text: this.account, placeholder: "用户名或邮箱" }) + TextInput({ text: this.account, placeholder: $r('app.string.add_secret_account_placeholder') }) .width("60%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.End) @@ -295,11 +247,11 @@ export struct AddSecret { .margin({ bottom: 10 }) Row() { - Text("密钥") + Text($r('app.string.add_secret_secret')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) - TextInput({ text: this.secret, placeholder: "由 A-Z,2-7 组成的字符串" }) + TextInput({ text: this.secret, placeholder: $r('app.string.add_secret_secret_placeholder') }) .width("60%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.End) @@ -309,7 +261,7 @@ export struct AddSecret { .margin({ bottom: 10 }) Row() { - Text("令牌位数") + Text($r('app.string.add_secret_digits')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -335,11 +287,15 @@ export struct AddSecret { .margin({ bottom: 10 }) Row() { - Button('添加') + Button($r('app.string.common_add')) .onClick(() => { try { if (this.secret.length <= 1 || !this.secret.match(/^[A-Z2-7]+$/)) { - throw new Error("密钥只能由 A-Z,2-7 组成,至少 2 个字符") + this.getUIContext().getPromptAction().showToast({ + duration: DefaultToastDuration, + message: $r('app.string.add_secret_invalid_secret') + }) + return } let encodedProvider = encodeURIComponent(this.provider) let encodedAccount = encodeURIComponent(this.account) @@ -350,7 +306,7 @@ export struct AddSecret { } catch (e) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `TOTP 错误:${e}` + message: $r('app.string.add_secret_error_format', `${e}`) }) } }) @@ -359,7 +315,7 @@ export struct AddSecret { } } } - .title("通过 TOTP 密钥添加") + .title($r('app.string.add_secret_title')) } } @@ -373,11 +329,11 @@ export struct AddUri { NavDestination() { Row() { Column() { - Text("输入 TOTP URI") + Text($r('app.string.add_uri_heading')) .fontSize(30) .fontWeight(FontWeight.Bold) .margin({ bottom: 10 }) - Text("如果需要同时添加多个,每行一个 URI") + Text($r('app.string.add_uri_desc')) .fontSize(14) .margin({ bottom: 20 }) TextArea({ text: "", placeholder: "otpauth://totp/xxx?secret=yyy" }) @@ -387,7 +343,7 @@ export struct AddUri { this.uri = value }) Row() { - Button('添加') + Button($r('app.string.common_add')) .onClick(() => { if (this.uri == "") { return @@ -406,7 +362,7 @@ export struct AddUri { } catch (e) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `已添加 ${count} 个,第 ${i + 1} 行的 TOTP URI 错误:${e}` + message: $r('app.string.add_uri_partial_error_format', count, i + 1, `${e}`) }) return } @@ -425,7 +381,7 @@ export struct AddUri { .width("90%") .height("90%") } - .title("通过 TOTP URI 添加") + .title($r('app.string.add_uri_title')) } } @@ -440,7 +396,7 @@ export struct AddSteamSecret { NavDestination() { Column() { Row() { - Text("提供方") + Text($r('app.string.add_secret_provider')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -454,11 +410,11 @@ export struct AddSteamSecret { .margin({ bottom: 10 }) Row() { - Text("账号") + Text($r('app.string.add_secret_account')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) - TextInput({ text: this.account, placeholder: "用户名或邮箱" }) + TextInput({ text: this.account, placeholder: $r('app.string.add_secret_account_placeholder') }) .width("60%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.End) @@ -470,11 +426,11 @@ export struct AddSteamSecret { .margin({ bottom: 10 }) Row() { - Text("密钥") + Text($r('app.string.add_secret_secret')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) - TextInput({ text: this.secret, placeholder: "由 A-Z,2-7 组成的字符串" }) + TextInput({ text: this.secret, placeholder: $r('app.string.add_secret_secret_placeholder') }) .width("60%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.End) @@ -484,7 +440,7 @@ export struct AddSteamSecret { .margin({ bottom: 10 }) Row() { - Text("令牌位数") + Text($r('app.string.add_secret_digits')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -498,11 +454,15 @@ export struct AddSteamSecret { .margin({ bottom: 10 }) Row() { - Button('添加') + Button($r('app.string.common_add')) .onClick(() => { try { if (this.secret.length <= 1 || !this.secret.match(/^[A-Z2-7]+$/)) { - throw new Error("密钥只能由 A-Z,2-7 组成,至少 2 个字符") + this.getUIContext().getPromptAction().showToast({ + duration: DefaultToastDuration, + message: $r('app.string.add_secret_invalid_secret') + }) + return } let encodedProvider = encodeURIComponent("Steam") let encodedAccount = encodeURIComponent(this.account) @@ -513,7 +473,7 @@ export struct AddSteamSecret { } catch (e) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `TOTP 错误:${e}` + message: $r('app.string.add_secret_error_format', `${e}`) }) } }) @@ -522,6 +482,6 @@ export struct AddSteamSecret { } } } - .title("添加 Steam 令牌") + .title($r('app.string.add_steam_secret_title')) } } \ No newline at end of file diff --git a/entry/src/main/ets/pages/AddSteam.ets b/entry/src/main/ets/pages/AddSteam.ets index 47f3e70..f175061 100644 --- a/entry/src/main/ets/pages/AddSteam.ets +++ b/entry/src/main/ets/pages/AddSteam.ets @@ -1,9 +1,8 @@ -import { AuthData, AuthDataStore, DefaultToastDuration, SteamAccountData, SteamAuthData } from "./Base"; -import { SteamClient } from "./Steam"; -import * as steamapi from './steamapi/custom' -import * as steam2faapi from './steamapi/service_twofactor' -import { uri, util } from "@kit.ArkTS"; -import { systemDateTime } from "@kit.BasicServicesKit"; +import { AuthData, AuthDataStore, DefaultToastDuration, SteamAccountData, SteamAuthData } from './Base'; +import { SteamClient } from './Steam'; +import * as steamapi from './steamapi/custom'; +import { uri, util } from '@kit.ArkTS'; +import { systemDateTime } from '@kit.BasicServicesKit'; @Component export struct AddSteamToken { @@ -13,13 +12,13 @@ export struct AddSteamToken { NavDestination() { RelativeContainer() { Column() { - Text("流程说明") + Text($r('app.string.steam_intro_title')) .fontWeight(FontWeight.Bold) .margin(10) - Text("1. 登录 Steam\n2. 向 Steam 中添加身份验证器") + Text($r('app.string.steam_intro_steps')) .copyOption(CopyOptions.LocalDevice) .fontSize(14) - Text("注:期间需要输入 Steam 密码和验证码。\n如遇请求超时或请求失败等问题,请在设\n置中开启增强访问。") + Text($r('app.string.steam_intro_note')) .copyOption(CopyOptions.LocalDevice) .fontSize(14) .margin(10) @@ -29,7 +28,7 @@ export struct AddSteamToken { center: { anchor: "__container__", align: VerticalAlign.Center } }) - Button("立即开始") + Button($r('app.string.steam_start_now')) .width(150) .margin({ bottom: 20 }) .alignRules({ @@ -41,7 +40,7 @@ export struct AddSteamToken { }) } } - .title("添加 Steam 令牌") + .title($r('app.string.add_steam_title')) } } @@ -60,13 +59,13 @@ export struct SteamLogin { RelativeContainer() { Column() { Row() { - Text("请输入 Steam 账户名称和密码") + Text($r('app.string.steam_login_prompt')) .fontWeight(FontWeight.Bold) } .margin({ bottom: 10 }) Row() { - Text("账户名称") + Text($r('app.string.steam_account_name')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -80,7 +79,7 @@ export struct SteamLogin { .margin({ bottom: 10 }) Row() { - Text("密码") + Text($r('app.string.steam_password')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -98,7 +97,7 @@ export struct SteamLogin { center: { anchor: "__container__", align: VerticalAlign.Center } }) - Button("登录") + Button($r('app.string.common_login')) .enabled((!this.loggingIn) && (this.steamAccount != "") && (this.steamPassword != "")) .width(150) .margin({ bottom: 20 }) @@ -109,7 +108,7 @@ export struct SteamLogin { .onClick(() => { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `开始登录 Steam...` + message: $r('app.string.detail_steam_login_start') }) this.loggingIn = true this.steamClient.login(this.steamAccount, this.steamPassword, (err, auth) => { @@ -117,7 +116,7 @@ export struct SteamLogin { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录失败:${err.message}` + message: $r('app.string.detail_steam_login_failed_format', err.message) }) return } @@ -138,15 +137,15 @@ export struct SteamLogin { if (!this.accountData.confirmation) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录成功,正在获取认证信息...` + message: $r('app.string.steam_login_fetching_auth') }) this.loggingIn = true - this.steamClient.pollTokens(auth!.clientId, auth!.requestId, (err, tokens) => { + this.steamClient.pollTokens(auth!.clientId, auth!.requestId, (pollErr, tokens) => { this.loggingIn = false - if (err) { + if (pollErr) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 认证信息获取失败:${err.message}` + message: $r('app.string.steam_auth_fetch_failed_format', pollErr.message) }) return } @@ -159,7 +158,7 @@ export struct SteamLogin { }) } } - .title("登录 Steam") + .title($r('app.string.steam_login_title')) } } @@ -178,7 +177,7 @@ export struct SteamMFALogin { if (this.accountData.confirmation!.confirmationType == steamapi.EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation) { Column() { - Text("Steam 启用了多重身份验证,请在其他设备上确认本次登录,确认完成后再点击【完成登录】") + Text($r('app.string.steam_mfa_device_confirmation')) .fontWeight(FontWeight.Bold) .margin(10) } @@ -189,7 +188,7 @@ export struct SteamMFALogin { } else if (this.accountData.confirmation!.confirmationType == steamapi.EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation) { Column() { - Text("Steam 启用了多重身份验证,确认邮件已发送,请点击邮件内的确认链接后再点击【完成登录】") + Text($r('app.string.steam_mfa_email_confirmation')) .fontWeight(FontWeight.Bold) .margin(10) } @@ -200,7 +199,7 @@ export struct SteamMFALogin { } else if (this.accountData.confirmation!.confirmationType == steamapi.EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode) { Column() { - Text("Steam 启用了多重身份验证,请从已绑定的身份验证器获取令牌并输入") + Text($r('app.string.steam_mfa_device_code')) .fontWeight(FontWeight.Bold) .margin(10) TextInput() @@ -216,7 +215,7 @@ export struct SteamMFALogin { } else if (this.accountData.confirmation!.confirmationType == steamapi.EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode) { Column() { - Text("Steam 启用了多重身份验证,令牌已通过邮件发送,请输入令牌") + Text($r('app.string.steam_mfa_email_code')) .fontWeight(FontWeight.Bold) .margin(10) TextInput() @@ -231,7 +230,7 @@ export struct SteamMFALogin { }) } - Button("完成登录") + Button($r('app.string.steam_complete_login')) .enabled(!this.loggingIn) .width(150) .margin({ bottom: 20 }) @@ -248,7 +247,7 @@ export struct SteamMFALogin { if (this.steamToken == "") { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `请先输入令牌` + message: $r('app.string.common_enter_token_first') }) return } @@ -259,21 +258,21 @@ export struct SteamMFALogin { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录失败:${err.message}` + message: $r('app.string.detail_steam_login_failed_format', err.message) }) return } this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录成功,正在获取认证信息...` + message: $r('app.string.steam_login_fetching_auth') }) this.loggingIn = true - this.steamClient.pollTokens(auth.clientId, auth.requestId, (err, tokens) => { + this.steamClient.pollTokens(auth.clientId, auth.requestId, (pollErr, tokens) => { this.loggingIn = false - if (err) { + if (pollErr) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 认证信息获取失败:${err.message}` + message: $r('app.string.steam_auth_fetch_failed_format', pollErr.message) }) return } @@ -284,7 +283,7 @@ export struct SteamMFALogin { } else { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `正在获取 Steam 认证信息...` + message: $r('app.string.steam_fetching_auth') }) this.loggingIn = true this.steamClient.pollTokens(auth.clientId, auth.requestId, (err, tokens) => { @@ -292,7 +291,7 @@ export struct SteamMFALogin { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 认证信息获取失败:${err.message}` + message: $r('app.string.steam_auth_fetch_failed_format', err.message) }) return } @@ -303,7 +302,7 @@ export struct SteamMFALogin { }) } } - .title("添加 Steam 令牌") + .title($r('app.string.add_steam_title')) } } @@ -319,7 +318,7 @@ export struct SteamLink { NavDestination() { RelativeContainer() { Column() { - Text(`${this.accountData.tokens!.accountName} 登录成功,请点击【添加】按钮将 Authenticator 添加到 Steam`) + Text($r('app.string.steam_link_success_format', this.accountData.tokens!.accountName)) .fontWeight(FontWeight.Bold) .margin(10) } @@ -328,7 +327,7 @@ export struct SteamLink { center: { anchor: "__container__", align: VerticalAlign.Center } }) - Button("添加") + Button($r('app.string.common_add')) .enabled(!this.linking) .width(150) .margin({ bottom: 20 }) @@ -344,7 +343,7 @@ export struct SteamLink { let tokens = this.accountData.tokens! this.linking = true this.steamClient.addAuthenticator(auth.steamid.toString(), tokens.refreshToken, this.accountData.deviceId, - (err, auth) => { + (err, authData) => { this.linking = false if (err) { if (err.message == "DuplicateRequest") { @@ -353,18 +352,18 @@ export struct SteamLink { } this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `添加 Authenticator 失败:${err.message}` + message: $r('app.string.steam_add_authenticator_failed_format', err.message) }) return } - this.accountData.authenticator = auth + this.accountData.authenticator = authData this.pageStack.pushPathByName("SteamConfirmLink", this.accountData) }) }) } } - .title("添加 Steam 令牌") + .title($r('app.string.add_steam_title')) } } @@ -383,7 +382,7 @@ export struct SteamConfirmLink { RelativeContainer() { if (this.accountData.authenticator!.confirmType == 1/*SMS*/) { Column() { - Text("正在向 Steam 添加身份验证器,请输入短信验证码") + Text($r('app.string.steam_confirm_link_sms')) .fontWeight(FontWeight.Bold) .margin(10) TextInput() @@ -398,7 +397,7 @@ export struct SteamConfirmLink { }) } else if (this.accountData.authenticator!.confirmType == 3/*Email*/) { Column() { - Text("正在向 Steam 添加身份验证器,验证码已通过邮件发送,请输入验证码") + Text($r('app.string.steam_confirm_link_email')) .fontWeight(FontWeight.Bold) .margin(10) TextInput() @@ -413,7 +412,7 @@ export struct SteamConfirmLink { }) } - Button("完成添加") + Button($r('app.string.steam_complete_add')) .enabled(!this.linking) .width(150) .margin({ bottom: 20 }) @@ -425,7 +424,7 @@ export struct SteamConfirmLink { if (this.steamToken == "") { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `请先输入验证码` + message: $r('app.string.common_enter_code_first') }) return } @@ -445,14 +444,14 @@ export struct SteamConfirmLink { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `添加身份验证器失败:${err.message}` + message: $r('app.string.steam_finalize_failed_format', err.message) }) return } if (resp!.wantMore) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `动态生成的令牌和 Steam 不一致,请再点击一次【完成添加】` + message: $r('app.string.steam_token_mismatch') }) return } @@ -468,6 +467,7 @@ export struct SteamConfirmLink { digits: 5, password: token, external: "", + tokenIconPath: "" } let helper = new util.Base64Helper() @@ -476,7 +476,7 @@ export struct SteamConfirmLink { steamID: auth.steamid.toString(), serialNumber: authenticator.serialNumber.toString(), revocationCode: authenticator.revocationCode, - shardSecret: helper.encodeToStringSync(authenticator.sharedSecret), + sharedSecret: helper.encodeToStringSync(authenticator.sharedSecret), tokenGID: authenticator.tokenGid, identitySecret: helper.encodeToStringSync(authenticator.identitySecret), uri: authenticator.uri, @@ -488,14 +488,14 @@ export struct SteamConfirmLink { this.authDataStore.addOneAuthItem(authData, steamAuthData) this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `添加成功` + message: $r('app.string.steam_add_success') }) this.pageStack.clear() }) }) } } - .title("添加 Steam 令牌") + .title($r('app.string.add_steam_title')) } } @@ -511,11 +511,11 @@ export struct SteamTransferAuthenticator { NavDestination() { RelativeContainer() { Column() { - Text("已经添加过其他身份验证器,是否迁移?") + Text($r('app.string.steam_transfer_prompt')) .fontWeight(FontWeight.Bold) .margin(10) - Text("特别说明:请确保 Steam 账户已经绑定了手机号码,否则将无法进行迁移") + Text($r('app.string.steam_transfer_note')) .fontWeight(FontWeight.Bold) .margin(10) } @@ -524,7 +524,7 @@ export struct SteamTransferAuthenticator { center: { anchor: "__container__", align: VerticalAlign.Center } }) - Button("迁移") + Button($r('app.string.steam_transfer_button')) .enabled(!this.linking) .width(150) .margin({ bottom: 20 }) @@ -541,7 +541,7 @@ export struct SteamTransferAuthenticator { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `迁移身份验证器失败:${err.message}` + message: $r('app.string.steam_transfer_failed_format', err.message) }) return } @@ -550,7 +550,7 @@ export struct SteamTransferAuthenticator { }) } } - .title("添加 Steam 令牌") + .title($r('app.string.add_steam_title')) } } @@ -568,7 +568,7 @@ export struct SteamConfirmTransferAuthenticator { NavDestination() { RelativeContainer() { Column() { - Text("正在迁移 Steam 身份验证器,请输入短信验证码") + Text($r('app.string.steam_transfer_sms')) .fontWeight(FontWeight.Bold) .margin(10) TextInput() @@ -582,7 +582,7 @@ export struct SteamConfirmTransferAuthenticator { center: { anchor: "__container__", align: VerticalAlign.Center } }) - Button("完成迁移") + Button($r('app.string.steam_complete_transfer')) .enabled(!this.linking && this.steamToken != "") .width(150) .margin({ bottom: 20 }) @@ -600,7 +600,7 @@ export struct SteamConfirmTransferAuthenticator { if (err || !account!.success) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `迁移身份验证器失败:${err?.message}` + message: $r('app.string.steam_transfer_failed_format', `${err?.message}`) }) return } @@ -615,6 +615,7 @@ export struct SteamConfirmTransferAuthenticator { digits: 5, password: "", external: "", + tokenIconPath: "" } let helper = new util.Base64Helper() @@ -623,7 +624,7 @@ export struct SteamConfirmTransferAuthenticator { steamID: auth.steamid.toString(), serialNumber: replacementToken.serialNumber!.toString(), revocationCode: replacementToken.revocationCode!, - shardSecret: helper.encodeToStringSync(replacementToken.sharedSecret), + sharedSecret: helper.encodeToStringSync(replacementToken.sharedSecret), tokenGID: replacementToken.tokenGid!, identitySecret: helper.encodeToStringSync(replacementToken.identitySecret), uri: replacementToken.uri!, @@ -635,14 +636,14 @@ export struct SteamConfirmTransferAuthenticator { this.authDataStore.addOneAuthItem(authData, steamAuthData) this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `添加成功` + message: $r('app.string.steam_add_success') }) this.pageStack.clear() }) }) } } - .title("添加 Steam 令牌") + .title($r('app.string.add_steam_title')) } } @@ -667,13 +668,13 @@ export struct SteamRefreshToken { RelativeContainer() { Column() { Row() { - Text("更新需要重新登录 Steam,请输入密码") + Text($r('app.string.steam_refresh_prompt')) .fontWeight(FontWeight.Bold) } .margin({ bottom: 10 }) Row() { - Text("账户名称") + Text($r('app.string.steam_account_name')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -685,7 +686,7 @@ export struct SteamRefreshToken { .margin({ bottom: 10 }) Row() { - Text("密码") + Text($r('app.string.steam_password')) .width("30%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -703,7 +704,7 @@ export struct SteamRefreshToken { center: { anchor: "__container__", align: VerticalAlign.Center } }) - Button("登录") + Button($r('app.string.common_login')) .enabled((!this.loggingIn) && (this.steamAccount != "") && (this.steamPassword != "")) .width(150) .margin({ bottom: 20 }) @@ -714,7 +715,7 @@ export struct SteamRefreshToken { .onClick(() => { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `开始登录 Steam...` + message: $r('app.string.detail_steam_login_start') }) this.loggingIn = true this.steamClient.login(this.steamAccount, this.steamPassword, (err, auth) => { @@ -722,7 +723,7 @@ export struct SteamRefreshToken { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录失败:${err.message}` + message: $r('app.string.detail_steam_login_failed_format', err.message) }) return } @@ -737,38 +738,38 @@ export struct SteamRefreshToken { if (confirmation != steamapi.EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录成功,但确认类型错误:${confirmation}` + message: $r('app.string.steam_refresh_confirmation_type_error_format', confirmation) }) return } this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录成功,正在提交令牌...` + message: $r('app.string.steam_refresh_submitting') }) this.loggingIn = true this.steamClient.submitLoginCode(auth!.steamid, auth!.clientId, - confirmation, this.authData.password, (err) => { + confirmation, this.authData.password, (submitErr) => { this.loggingIn = false - if (err) { + if (submitErr) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 令牌提交失败:${err.message}` + message: $r('app.string.steam_refresh_submit_failed_format', submitErr.message) }) return } this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 令牌提交成功,正在获取认证信息...` + message: $r('app.string.steam_refresh_submit_success') }) this.loggingIn = true - this.steamClient.pollTokens(auth!.clientId, auth!.requestId, (err, tokens) => { + this.steamClient.pollTokens(auth!.clientId, auth!.requestId, (pollErr, tokens) => { this.loggingIn = false - if (err) { + if (pollErr) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 认证信息获取失败:${err.message}` + message: $r('app.string.steam_auth_fetch_failed_format', pollErr.message) }) return } @@ -778,7 +779,7 @@ export struct SteamRefreshToken { this.authDataStore.saveAuthItem(this.authData.id) this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `更新 Steam 认证信息成功` + message: $r('app.string.steam_refresh_success') }) this.pageStack.clear() }) @@ -787,6 +788,6 @@ export struct SteamRefreshToken { }) } } - .title("更新 Steam 认证信息过期时间") + .title($r('app.string.steam_refresh_title')) } } \ No newline at end of file diff --git a/entry/src/main/ets/pages/Base.ets b/entry/src/main/ets/pages/Base.ets index 928656d..ecaea8b 100644 --- a/entry/src/main/ets/pages/Base.ets +++ b/entry/src/main/ets/pages/Base.ets @@ -1,15 +1,16 @@ -import { systemDateTime } from '@kit.BasicServicesKit'; -import { ComponentContent } from "@kit.ArkUI"; +import { systemDateTime } from '@kit.BasicServicesKit'; +import { ComponentContent } from '@kit.ArkUI'; import { cryptoFramework } from '@kit.CryptoArchitectureKit'; import { buffer, JSON, uri, util } from '@kit.ArkTS'; import { distributedKVStore } from '@kit.ArkData'; -import { kvManager } from '../entryability/EntryAbility'; -import * as steamapi from './steamapi/custom' -import * as steam2faapi from './steamapi/service_twofactor' -import * as googleapi from './googleapi/otpauth_migrate' +import { getLocalizedString, kvManager } from '../entryability/EntryAbility'; +import * as steamapi from './steamapi/custom'; +import * as steam2faapi from './steamapi/service_twofactor'; +import * as googleapi from './googleapi/otpauth_migrate'; export let DefaultToastDuration = 5000 + export class AuthData { id: number = 0 alias: string = "" @@ -19,6 +20,7 @@ export class AuthData { digits: number = 6 password: string = "" external: string = "" + tokenIconPath: string = "" } export class SteamAuthData { @@ -26,7 +28,7 @@ export class SteamAuthData { public steamID: string = "" public serialNumber: string = "" public revocationCode: string = "" - public shardSecret: string = "" + public sharedSecret: string = "" public tokenGID: string = "" public identitySecret: string = "" public uri: string = "" @@ -47,6 +49,31 @@ export class SteamAccountData { account?: steam2faapi.CTwoFactor_RemoveAuthenticatorViaChallengeContinue_Response } +export function isLegacySteamExternal(authdata: Record): boolean { + return authdata["sharedSecret"] == undefined && authdata["shardSecret"] != undefined +} + +export function parseSteamExternalAuthData(authdata: Record): SteamAuthData { + let steamAuthData = new SteamAuthData() + steamAuthData.sharedSecret = authdata["sharedSecret"] as string + if (steamAuthData.sharedSecret == undefined) { + // 兼容旧版本导出的字段名:shardSecret + steamAuthData.sharedSecret = authdata["shardSecret"] as string + } + steamAuthData.accountName = authdata["accountName"] as string + steamAuthData.steamID = authdata["steamID"] as string + steamAuthData.serialNumber = authdata["serialNumber"] as string + steamAuthData.revocationCode = authdata["revocationCode"] as string + steamAuthData.tokenGID = authdata["tokenGID"] as string + steamAuthData.identitySecret = authdata["identitySecret"] as string + steamAuthData.uri = authdata["uri"] as string + steamAuthData.deviceID = authdata["deviceID"] as string + steamAuthData.secret1 = authdata["secret1"] as string + steamAuthData.accessToken = authdata["accessToken"] as string + steamAuthData.refreshToken = authdata["refreshToken"] as string + return steamAuthData +} + export class AuthDataStore { public kvStore?: distributedKVStore.DeviceKVStore public items: AuthData[] = [] @@ -60,13 +87,17 @@ export class AuthDataStore { securityLevel: distributedKVStore.SecurityLevel.S2, }, (err: Error, store: distributedKVStore.DeviceKVStore) => { if (err) { - throw err + throw new Error(err.message) } this.kvStore = store this.kvStore.getEntries("/data/auth/", (err, data) => { for (let i = 0; i < data.length; i++) { let element = data[i]; let authdata = JSON.parse(element.value.value as string) as AuthData + // 兼容之前的版本 + if (authdata.tokenIconPath == undefined) { + authdata.tokenIconPath = ""; + } authdata.password = this.generatePassword(authdata) this.items.push(authdata) if (authdata.id >= this.next) { @@ -210,6 +241,15 @@ export class AuthDataStore { this.onChanged(this.items) } + updateTokenIconPath(id: number, tokenIconPath: string) { + let index = this.findAuthItem(id) + if (index < 0) { + return + } + this.items[index].tokenIconPath = tokenIconPath + this.saveAuthItem(id) + } + addGoogleAuthItem(data: string) { let base64 = new util.Base64Helper() let bytes = base64.decodeSync(data, util.Type.BASIC_URL_SAFE) @@ -229,6 +269,7 @@ export class AuthDataStore { digits: parameter.digits == googleapi.MigrationPayload.DigitCount.DIGIT_COUNT_EIGHT ? 8 : 6, password: "", external: "", + tokenIconPath: "" } authdata.password = this.generatePassword(authdata) this.next++ @@ -248,7 +289,8 @@ export class AuthDataStore { return } if (authURI.scheme != "otpauth" || authURI.host != "totp") { - throw new Error(`无效的协议:${authURI.scheme}://${authURI.host}`); + throw new Error(getLocalizedString($r('app.string.base_invalid_scheme_format'), + `${authURI.scheme}://${authURI.host}`)); } let secret = authURI.getQueryValue("secret") let issuer = authURI.getQueryValue("issuer") @@ -271,6 +313,7 @@ export class AuthDataStore { digits: digits == "8" ? 8 : digits == "7" ? 7 : 6, password: "", external: "", + tokenIconPath: "" } if (issuer == "Steam") { authdata.digits = 5 @@ -288,19 +331,39 @@ export class AuthDataStore { recoverAuthItem(data: string) { let result = JSON.parse(data) as AuthData[] if (!result) { - throw new Error("未知的文件格式") + throw new Error(getLocalizedString($r('app.string.base_unknown_file_format'))) } for (let index = 0; index < result.length; index++) { const element = result[index]; Object.entries(element).forEach((pair: [string, string]) => { + if (pair[0] == 'password' || pair[0] == 'tokenIconPath') { + return; + } if (pair[1] == undefined) { - throw new Error(`第 ${index} 个令牌密钥缺少 ${pair[0]}`) + throw new Error(getLocalizedString($r('app.string.base_recover_item_missing_format'), index, pair[0])) } }) } for (let index = 0; index < result.length; index++) { const element = result[index]; + if (element.issuer.toLowerCase() == 'steam') { + if (element.external == '') { + continue; + } + let authdata = JSON.parse(element.external) as Record + // 兼容旧版备份中的 shardSecret 字段并统一结构 + if (isLegacySteamExternal(authdata)) { + let steamAuthData = parseSteamExternalAuthData(authdata) + Object.entries(steamAuthData).forEach((pair: [string, string]) => { + if (pair[1] == undefined) { + throw new Error(getLocalizedString($r('app.string.base_missing_format'), pair[0])) + } + }) + element.external = JSON.stringify(steamAuthData) + } + } element.id = this.next++ + if (this.kvStore) { let authString = JSON.stringify(element) this.kvStore.put(`/data/auth/${element.id}`, authString) @@ -355,8 +418,8 @@ export class PromptParams { constructor(ctx: UIContext, builder: WrappedBuilder<[PromptParams]>, data: T) { this.ctx = ctx - this.content = new ComponentContent(this.ctx, builder, this); this.data = data + this.content = new ComponentContent(this.ctx, builder, this); } open() { diff --git a/entry/src/main/ets/pages/Detail.ets b/entry/src/main/ets/pages/Detail.ets index a8d0a8e..ec2fa38 100644 --- a/entry/src/main/ets/pages/Detail.ets +++ b/entry/src/main/ets/pages/Detail.ets @@ -1,9 +1,20 @@ -import { AuthData, AuthDataStore, DefaultToastDuration, PromptParams, SteamAuthData } from "./Base" -import { SymbolGlyphModifier } from "@kit.ArkUI"; -import { JSON, util } from "@kit.ArkTS"; -import { intl } from "@kit.LocalizationKit"; -import { scanBarcode, scanCore } from "@kit.ScanKit"; -import { SteamClient } from "./Steam"; +import { + AuthData, + AuthDataStore, + DefaultToastDuration, + parseSteamExternalAuthData, + PromptParams, + SteamAuthData +} from './Base'; +import { SymbolGlyphModifier } from '@kit.ArkUI'; +import { JSON, util } from '@kit.ArkTS'; +import { intl } from '@kit.LocalizationKit'; +import { scanBarcode, scanCore } from '@kit.ScanKit'; +import { SteamClient } from './Steam'; +import { TokenIcon } from '../components/TokenIcon'; +import { buildTokenIconPicker, TokenIconPickerData } from '../components/TokenIconPicker'; +import { getLocalizedString } from '../entryability/EntryAbility'; + class SteamParams { public maFile: string = "" @@ -18,10 +29,10 @@ class SteamParams { function buildSteamInput(params: PromptParams) { Row() { Column() { - Text("输入 Steam 认证信息") + Text($r('app.string.detail_steam_input_title')) .fontSize(30) .fontWeight(FontWeight.Bold) - Text("认证通过后可以使用 Steam 扫码登陆和交易确认功能") + Text($r('app.string.detail_steam_input_desc')) .fontSize(12) .fontWeight(FontWeight.Normal) .margin({ bottom: 20 }) @@ -32,10 +43,10 @@ function buildSteamInput(params: PromptParams) { params.data.maFile = value }) Row() { - Button('取消').onClick(() => { + Button($r('app.string.common_cancel')).onClick(() => { params.close() }) - Button('确认').onClick(() => { + Button($r('app.string.common_confirm')).onClick(() => { try { let data = new SteamAuthData() let authdata = JSON.parse(params.data.maFile, (key: string, val: Object | undefined | null) => { @@ -48,7 +59,7 @@ function buildSteamInput(params: PromptParams) { data.steamID = authdata["steam_id"] as string data.serialNumber = authdata["serial_number"] as string data.revocationCode = authdata["revocation_code"] as string - data.shardSecret = authdata["shared_secret"] as string + data.sharedSecret = authdata["shared_secret"] as string data.tokenGID = authdata["token_gid"] as string data.identitySecret = authdata["identity_secret"] as string data.uri = authdata["uri"] as string @@ -57,10 +68,9 @@ function buildSteamInput(params: PromptParams) { let tokens = authdata["tokens"] as Record data.accessToken = tokens["access_token"] data.refreshToken = tokens["refresh_token"] - Object.entries(data).forEach((pair: [string, string]) => { if (pair[1] == undefined) { - throw new Error(`缺少 ${pair[0]}`) + throw new Error(getLocalizedString($r('app.string.base_missing_format'), pair[0])) } }) @@ -70,11 +80,10 @@ function buildSteamInput(params: PromptParams) { params.close() detail.authDataStore.saveAuthItem(detail.item.id) params.data.detail?.loadSteamAuthData() - } catch (e) { params.data.detail?.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `认证信息格式错误,请确保内容来自 maFile 文件,${e.toString()}` + message: $r('app.string.detail_steam_input_error_format', `${e.toString()}`) }) return } @@ -119,14 +128,16 @@ export struct Detail { } loadSteamAuthData(): void { - if (this.item.issuer != "Steam" || this.item.external == "") { + if (this.item.issuer.toLowerCase() != "steam" || this.item.external == "") { return } if (this.item.external == this.lastExternalInfo) { return } try { - this.steamAuthData = JSON.parse(this.item.external) as SteamAuthData + let authdata = JSON.parse(this.item.external) as Record + this.steamAuthData = parseSteamExternalAuthData(authdata) + let parts = this.steamAuthData.refreshToken.split(".") let base64 = new util.Base64Helper() let data = base64.decodeSync(parts[1], util.Type.BASIC_URL_SAFE) @@ -139,7 +150,7 @@ export struct Detail { } catch (e) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `认证信息格式错误,${e.toString()}, 内容:${this.item.external}` + message: $r('app.string.detail_steam_invalid_error_format', `${e.toString()}`, this.item.external) }) } } @@ -161,29 +172,29 @@ export struct Detail { try { scanBarcode.startScanForResult(getContext(this), options).then((result: scanBarcode.ScanResult) => { this.steamClient.approveQRLogin(this.steamAuthData.steamID, this.steamAuthData.refreshToken, - this.steamAuthData.shardSecret, false, result.originalValue, + this.steamAuthData.sharedSecret, false, result.originalValue, (err) => { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `Steam 登录失败:${err.message}` + message: $r('app.string.detail_steam_login_failed_format', err.message) }) return } this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "Steam 登录成功" + message: $r('app.string.detail_steam_login_success') }) }) this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "开始登录 Steam..." + message: $r('app.string.detail_steam_login_start') }) }) } catch (error) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `扫码失败:${error}` + message: $r('app.string.common_scan_failed_format', `${error}`) }) } } @@ -193,21 +204,37 @@ export struct Detail { this.pageStack.pushPathByName("SteamConfirmationList", params) } + openTokenIconPicker() { + const data = new TokenIconPickerData() + data.issuer = this.item.issuer + data.currentIconPath = this.item.tokenIconPath + let params: PromptParams + data.onConfirm = (path: string): void => { + this.item.tokenIconPath = path + this.authDataStore.updateTokenIconPath(this.item.id, path) + } + data.onClose = (): void => { + params.close() + } + params = new PromptParams(this.getUIContext(), wrapBuilder(buildTokenIconPicker), data) + params.open() + } + @Builder buildExternalView() { - if (this.item.issuer == "Steam") { + if (this.item.issuer.toLowerCase() == "steam") { if (this.item.external != undefined && this.item.external.length > 0) { Row() { Column() { - Text("----- Steam 认证信息 -----") - Text("如遇扫码登录和交易确认出现请求超时或请求失败等问题,请在设置中开启增强访问。") + Text($r('app.string.detail_steam_section_title')) + Text($r('app.string.detail_steam_section_desc')) .padding({ left: 30, right: 30 }) .fontSize(12) } } Row() { - Text("Steam 账号") + Text($r('app.string.detail_steam_account')) .width("30%") .textAlign(TextAlign.Start) .margin(5) @@ -219,7 +246,7 @@ export struct Detail { .alignItems(VerticalAlign.Center) Row() { - Text("Steam ID") + Text($r('app.string.detail_steam_id')) .width("30%") .textAlign(TextAlign.Start) Text(this.steamAuthData.steamID) @@ -230,7 +257,7 @@ export struct Detail { .alignItems(VerticalAlign.Center) Row() { - Text("设备 ID") + Text($r('app.string.detail_device_id')) .width("30%") .textAlign(TextAlign.Start) Text(this.steamAuthData.deviceID.substring(this.steamAuthData.deviceID.indexOf(":") + 1)) @@ -241,7 +268,7 @@ export struct Detail { .alignItems(VerticalAlign.Center) Row() { - Text("过期时间") + Text($r('app.string.detail_expire_time')) .width("30%") .textAlign(TextAlign.Start) Text(this.steamExpirationTime.toLocaleString((new intl.Locale()).toString(), { hour12: false })) @@ -252,17 +279,17 @@ export struct Detail { .alignItems(VerticalAlign.Center) Row() { - Button("重新设置") + Button($r('app.string.detail_reset')) .onClick(() => this.onSteamButtonPressed()) .width("40%") .margin(5) - Button("更新过期时间") + Button($r('app.string.detail_update_expire')) .onClick(() => this.onSteamRefreshButtonPressed()) .width("40%") .margin(5) } } else { - Button("设置 Steam 认证信息") + Button($r('app.string.detail_setup_steam')) .onClick(() => this.onSteamButtonPressed()) } } @@ -272,18 +299,28 @@ export struct Detail { NavDestination() { Column() { Row() { + Stack() { + TokenIcon({ issuer: this.item.issuer, iconPath: this.item.tokenIconPath }) + } + .gesture( + LongPressGesture() + .onAction(() => { + this.openTokenIconPicker() + }) + ) + Text(this.safe ? this.item.password : "******") .fontSize(40) .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Center) .copyOption(CopyOptions.LocalDevice) } - .height(40) + .height(60) .alignItems(VerticalAlign.Center) .margin(20) Row() { - Text("别名") + Text($r('app.string.detail_alias')) .width("30%") .textAlign(TextAlign.Start) TextInput({ text: this.item.alias }) @@ -302,7 +339,7 @@ export struct Detail { .alignItems(VerticalAlign.Center) Row() { - Text("账号") + Text($r('app.string.detail_account')) .width("30%") .textAlign(TextAlign.Start) Text(this.item.account) @@ -313,7 +350,7 @@ export struct Detail { .alignItems(VerticalAlign.Center) Row() { - Text("发行商") + Text($r('app.string.detail_issuer')) .width("30%") .textAlign(TextAlign.Start) Text(this.item.issuer) @@ -324,7 +361,7 @@ export struct Detail { .alignItems(VerticalAlign.Center) Row() { - Text("位数") + Text($r('app.string.detail_digits')) .width("30%") .textAlign(TextAlign.Start) Text(this.item.digits.toString()) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 33bef22..db49198 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -1,5 +1,5 @@ -import { SymbolGlyphModifier } from "@ohos.arkui.modifier" -import { scanCore, scanBarcode } from '@kit.ScanKit'; +import { SymbolGlyphModifier } from '@ohos.arkui.modifier'; +import { scanBarcode, scanCore } from '@kit.ScanKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { BusinessError, systemDateTime } from '@kit.BasicServicesKit'; import { cryptoFramework } from '@kit.CryptoArchitectureKit'; @@ -10,7 +10,7 @@ import { AuthData, AuthDataStore, DefaultToastDuration, PromptParams, SteamAccou import { SteamConfirmationList } from './SteamConfirmationList'; import { SteamConfirmationDetail } from './SteamConfirmationDetail'; import { Confirmation } from './Steam'; -import { Add, AddSecret, AddSteamSecret, AddUri } from "./Add"; +import { Add, AddSecret, AddSteamSecret, AddUri } from './Add'; import { AddSteamToken, SteamConfirmLink, @@ -18,11 +18,14 @@ import { SteamLink, SteamLogin, SteamMFALogin, - SteamTransferAuthenticator, - SteamRefreshToken -} from "./AddSteam"; -import { ShowPrivacyView } from "./PrivacyView"; -import { curves } from "@kit.ArkUI"; + SteamRefreshToken, + SteamTransferAuthenticator +} from './AddSteam'; +import { ShowPrivacyView } from './PrivacyView'; +import { curves } from '@kit.ArkUI'; +import { TokenIcon } from '../components/TokenIcon'; +import { buildTokenIconPicker, TokenIconPickerData } from '../components/TokenIconPicker'; +import { getLocalizedString } from '../entryability/EntryAbility'; @Entry @Component @@ -55,7 +58,6 @@ struct Index { this.privacyState = state }) } - } aboutToDisappear(): void { @@ -104,7 +106,7 @@ struct Index { }; // 配置认证界面 const widgetParam: userAuth.WidgetParam = { - title: '请进行身份认证', + title: getLocalizedString($r('app.string.index_auth_title')), }; // 获取认证对象 const userAuthInstance = userAuth.getUserAuthInstance(authParam, widgetParam); @@ -132,6 +134,28 @@ struct Index { this.safe = false } + showSafeModeToast() { + this.getUIContext().getPromptAction().showToast({ + duration: DefaultToastDuration, + message: $r('app.string.common_safe_mode_unlock_first') + }) + } + + openTokenIconPicker(item: AuthData) { + const data = new TokenIconPickerData() + data.issuer = item.issuer + data.currentIconPath = item.tokenIconPath + let params: PromptParams + data.onConfirm = (path: string): void => { + this.authDataStore.updateTokenIconPath(item.id, path) + } + data.onClose = (): void => { + params.close() + } + params = new PromptParams(this.getUIContext(), wrapBuilder(buildTokenIconPicker), data) + params.open() + } + @Builder PagesMap(name: string, param: object) { if (name == 'Add') { @@ -169,48 +193,6 @@ struct Index { } } - private vendors: Map= new Map([ - ["google", "Google.png"], - ["microsoft", "Microsoft.png"], - ["github", "Github.png"], - ["gitlab", "Gitlab.png"], - ["steam", "Steam.png"], - ["huawei", "Huawei.png"], - ["aliyun", "Aliyun.png"], - ["namecheap", "Namecheap.png"], - ["neteasemail", "NeteaseMail.png"], - ["nintendo", "Nintendo.png"], - ["nintendo account", "Nintendo.png"], - ["epic games", "Epic Games.png"], - ["cloudflare", "Cloudflare.png"], - ["twitter", "Twitter.png"], - ["nvidia", "Nvidia.png"], - ["ubisoft", "Ubisoft.png"], - ["uplay", "Ubisoft.png"], - ["jetbrains", "Jetbrains.png"], - ]); - - @Builder - Logo(issuer: string) { - if (this.vendors.has(issuer.toLowerCase())) { - Image($rawfile(this.vendors.get(issuer.toLowerCase()))) - .width(40) - .padding(5) - .margin(10) - .backgroundColor($r("app.color.vendor_background_color")) - .borderRadius(20) - .draggable(false) - } else { - SymbolGlyph($r("sys.symbol.person")) - .fontSize(30) - .padding(5) - .margin(10) - .fontColor([$r("app.color.vendor_icon_color")]) - .backgroundColor($r("app.color.vendor_background_color")) - .borderRadius(20) - } - } - build() { Column() { Navigation(this.pageStack) { @@ -219,7 +201,7 @@ struct Index { Column() { SymbolGlyph($r("sys.symbol.info_shield")) .fontSize(35) - Text("还没有添加过令牌密钥\n请点击右上角的加号或扫码按钮添加") + Text($r('app.string.index_empty')) .fontWeight(FontWeight.Bold) .padding(20) .textAlign(TextAlign.Center) @@ -237,7 +219,20 @@ struct Index { ForEach(this.items, (item: AuthData, index: number) => { ListItem() { Row() { - this.Logo(item.issuer) + Stack() { + TokenIcon({ issuer: item.issuer, iconPath: item.tokenIconPath }) + } + .gesture( + LongPressGesture() + .onAction(() => { + if (!this.safe) { + this.showSafeModeToast() + return + } + this.openTokenIconPicker(item) + }) + ) + Column() { Text(item.alias) .fontSize(20) @@ -253,10 +248,7 @@ struct Index { .fontWeight(FontWeight.Bold) .onClick(() => { if (!this.safe) { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已启用安全模式,请先解锁" - }) + this.showSafeModeToast() return } if (!this.hideToken) { @@ -270,10 +262,7 @@ struct Index { .opacity(item.id == this.dragItem?.id ? 0 : 1) .onClick(() => { if (!this.safe) { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已启用安全模式,请先解锁" - }) + this.showSafeModeToast() return } this.pageStack.pushPathByName("Detail", item) @@ -323,7 +312,8 @@ struct Index { this.dragItem = undefined }) }) - }, (item: AuthData) => item.id.toString() + "-" + item.alias + "-" + item.account + "-" + item.password) + }, (item: AuthData) => item.id.toString() + "-" + item.alias + "-" + item.account + "-" + item.password + + "-" + item.tokenIconPath) } } } @@ -344,10 +334,7 @@ struct Index { symbolIcon: new SymbolGlyphModifier($r('sys.symbol.gearshape')), action: () => { if (!this.safe) { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已启用安全模式,请先解锁" - }) + this.showSafeModeToast() return } this.pageStack.pushPathByName("Setting", undefined) @@ -378,12 +365,9 @@ struct Index { .type(ButtonType.Normal) .width(60) .height("100%") - .onClick((event) => { + .onClick(() => { if (!this.safe) { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已启用安全模式,请先解锁" - }) + this.showSafeModeToast() return } this.authDataStore.removeAuthItem(id) @@ -393,10 +377,7 @@ struct Index { onAddButtonPressed() { if (!this.safe) { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已启用安全模式,请先解锁" - }) + this.showSafeModeToast() return } this.pageStack.pushPathByName("Add", undefined) @@ -404,10 +385,7 @@ struct Index { onScanButtonPressed() { if (!this.safe) { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已启用安全模式,请先解锁" - }) + this.showSafeModeToast() return } let options: scanBarcode.ScanOptions = @@ -422,19 +400,19 @@ struct Index { this.authDataStore.addAuthItem(result.originalValue) this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "成功添加令牌" + message: $r('app.string.index_add_success') }) } catch (error) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `扫码失败:${error}` + message: $r('app.string.common_scan_failed_format', `${error}`) }) } }) } catch (error) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `扫码失败:${error}` + message: $r('app.string.common_scan_failed_format', `${error}`) }) } } @@ -454,20 +432,20 @@ struct Index { if (steamTokenCount <= 0) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "没有 Steam 令牌,无法扫码登陆 Steam,请先添加 Steam 令牌" + message: $r('app.string.index_no_steam_token') }) return } if (steamAuthDataCount <= 0) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "Steam 令牌没有设置认证信息,无法扫码登陆 Steam,请在令牌详情页设置" + message: $r('app.string.index_no_steam_auth') }) return } this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "请在 Steam 令牌详情页扫码登陆" + message: $r('app.string.index_go_to_steam_detail') }) return } diff --git a/entry/src/main/ets/pages/PrivacyView.ets b/entry/src/main/ets/pages/PrivacyView.ets index f5a348a..901ed0a 100644 --- a/entry/src/main/ets/pages/PrivacyView.ets +++ b/entry/src/main/ets/pages/PrivacyView.ets @@ -14,48 +14,14 @@ export function ShowPrivacyView(context: UIContext, setState: (state: string) => view.open() } - @Builder function PrivacyView(params: PromptParams) { Column() { - Text("隐私政策") + Text($r('app.string.privacy_title')) .fontSize(20) .margin({ top: 10 }) Scroll() { - Text(`更新日期:2025/04/03 -生效日期:2025/04/03 -导言 -千维验证器是一款由 郭维 (以下简称“我们”)提供的产品。 您在使用我们的服务时,我们不会收集和使用您的相关信息,我们希望通过本《隐私政策》向您说明。本《隐私政策》与您所使用的千维验证器服务息息相关,希望您仔细阅读,在需要时,按照本《隐私政策》的指引,作出您认为适当的选择。本《隐私政策》中涉及的相关技术词汇,我们尽量以简明扼要的表述,并提供进一步说明的链接,以便您的理解。 - -您使用或继续使用我们的服务,即意味着同意本《隐私政策》。 - -感谢您使用我们的服务!我们重视您的隐私,并承诺保护您的个人信息。在使用本服务时,我们希望向您明确声明,我们不会主动收集、存储或共享任何个人信息。请您在使用本服务前,仔细阅读本隐私政策。 - -1. 我们不收集个人信息 -在您使用本服务的过程中,我们不会主动收集您的任何个人信息。无论您是匿名使用服务,还是以其他方式与我们进行互动,我们都不会要求或获取您的姓名、电子邮件地址、电话号码、住址、支付信息等个人身份信息。 - -2. 不涉及第三方数据共享 -由于我们不收集任何个人信息,我们也不会将您的任何数据与第三方共享、出售或租赁。您的隐私安全对我们来说至关重要,我们不会向任何第三方提供与您身份相关的任何数据。 - -3. 技术数据收集 -尽管我们不会收集您的个人信息,但我们可能会使用一些常见的技术手段来收集非个人信息,例如您的设备类型、浏览器类型、操作系统版本、访问日期和时间等。这些信息仅用于改善服务质量和用户体验,不会用于识别您个人身份。 - -4. 数据存储 -由于我们不会收集个人信息,因此不会在服务器上存储任何与您身份相关的个人数据。任何与服务有关的日志数据都仅用于技术分析和系统维护,且不会用于任何身份识别。 - -5. 安全性 -虽然我们不收集您的个人信息,但我们依然采取合理的安全措施来保护我们服务的数据安全。这些措施包括但不限于加密技术、服务器安全和网络防护,以确保我们的系统免受外部攻击或数据泄露。 - -6. 儿童隐私 -我们的服务并不针对儿童,我们不会 knowingly 收集或请求未满18岁用户的个人信息。如果我们发现收集了未满18岁用户的个人信息,我们将尽快删除相关数据。 - -7. 政策变更 -我们可能会根据法律、政策或服务改进的需要,适时更新或修改本隐私政策。如果本隐私政策发生重要变更,我们将在适当的地方进行通知,以确保您及时了解我们的隐私保护措施。 - -8. 联系我们 -如果您对本隐私政策有任何疑问或需要更多信息,请随时联系我们。 -` - ) + Text($r('app.string.privacy_content')) } .backgroundColor("#00000000") .height(300) @@ -64,14 +30,14 @@ function PrivacyView(params: PromptParams) { Column() { Row() { - Button("不同意") + Button($r('app.string.privacy_disagree')) .width("30%") .margin({ right: 20 }) .onClick(() => { params.data.setState("Disagreed") params.close() }) - Button("同意") + Button($r('app.string.privacy_agree')) .width("30%") .margin({ left: 20 }) .onClick(() => { diff --git a/entry/src/main/ets/pages/Setting.ets b/entry/src/main/ets/pages/Setting.ets index a6a4d87..8b31def 100644 --- a/entry/src/main/ets/pages/Setting.ets +++ b/entry/src/main/ets/pages/Setting.ets @@ -1,8 +1,9 @@ -import { bundleManager, common, ConfigurationConstant } from "@kit.AbilityKit"; -import { picker, fileIo as fs } from "@kit.CoreFileKit"; -import { AuthDataStore, DefaultToastDuration } from "./Base"; -import { ShowPrivacyView } from "./PrivacyView"; -import { proxyServer } from "./proxy/Proxy" +import { bundleManager, common, ConfigurationConstant } from '@kit.AbilityKit'; +import { fileIo as fs, picker } from '@kit.CoreFileKit'; +import { AuthDataStore, DefaultToastDuration, parseSteamExternalAuthData } from './Base'; +import { ShowPrivacyView } from './PrivacyView'; +import { proxyServer } from './proxy/Proxy'; +import { IconPackManager } from '../components/IconPackManager'; @Component export struct Setting { @@ -20,12 +21,17 @@ export struct Setting { this.appInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION) } + getPrivacyState(): Resource { + return this.privacyState == "Agreed" ? $r('app.string.setting_privacy_agreed') : + $r('app.string.setting_privacy_disagreed') + } + build() { NavDestination() { RelativeContainer() { Column() { Row() { - Text("跟随系统深色模式") + Text($r('app.string.setting_auto_dark')) .width("80%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -44,14 +50,14 @@ export struct Setting { .alignItems(VerticalAlign.Center) Row() { - Text("启用后,系统打开或关闭深色模式时,App 也跟随切换。") + Text($r('app.string.setting_auto_dark_desc')) .padding({ left: 30, right: 20 }) .fontSize(12) } .width("100%") Row() { - Text("启用安全模式") + Text($r('app.string.setting_safe_mode')) .width("80%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -64,14 +70,14 @@ export struct Setting { .alignItems(VerticalAlign.Center) Row() { - Text("启用后,打开 App 时需要通过生物识别(人脸识别,指纹,密码)解锁,解锁成功后才能查看令牌。\n启用后会在下次打开 App 时生效。") + Text($r('app.string.setting_safe_mode_desc')) .padding({ left: 30, right: 20 }) .fontSize(12) } .width("100%") Row() { - Text("隐藏令牌") + Text($r('app.string.setting_hide_token')) .width("80%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -84,15 +90,19 @@ export struct Setting { .alignItems(VerticalAlign.Center) Row() { - Text("启用后,在 App 首页的令牌将显示为星号,点击星号显示令牌。\n详情页不受此设置影响,直接显示令牌。") + Text($r('app.string.setting_hide_token_desc')) .padding({ left: 30, right: 20 }) .fontSize(12) } .width("100%") + IconPackManager() + .margin({ top: 12, bottom: 12 }) + .padding({ left: 20, right: 20 }) + if (proxyServer != "") { Row() { - Text("增强访问") + Text($r('app.string.setting_network')) .width("80%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) @@ -101,17 +111,11 @@ export struct Setting { if (this.developmentCounter >= 10) { this.developmentCounter = 0 this.developmentMode = !this.developmentMode - if (this.developmentMode) { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已启用开发者模式" - }) - } else { - this.getUIContext().getPromptAction().showToast({ - duration: DefaultToastDuration, - message: "已关闭开发者模式" - }) - } + this.getUIContext().getPromptAction().showToast({ + duration: DefaultToastDuration, + message: this.developmentMode ? $r('app.string.setting_network_enabled') : + $r('app.string.setting_network_disabled') + }) } }) Toggle({ type: ToggleType.Switch, isOn: this.enhanceNetwork }) @@ -123,7 +127,7 @@ export struct Setting { .alignItems(VerticalAlign.Center) Row() { - Text("启用后,增强通过令牌访问特定服务的稳定性。") + Text($r('app.string.setting_network_desc')) .padding({ left: 30, right: 20 }) .fontSize(12) } @@ -131,11 +135,11 @@ export struct Setting { } Row() { - Text("隐私政策") + Text($r('app.string.setting_privacy_policy')) .width("75%") .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Start) - Text(this.privacyState == "Agreed" ? "已同意" : "未同意") + Text(this.getPrivacyState()) .width("15%") .fontWeight(FontWeight.Bold) .onClick(() => { @@ -143,22 +147,17 @@ export struct Setting { this.privacyState = state }) }) - } .height(40) .alignItems(VerticalAlign.Center) - Row() { - Text("无论是否同意隐私政策,均可使用所有功能。点击选项右侧的【" + (this.privacyState == "Agreed" ? "已同意" : - "未同意") + "】,可再次查看隐私政策内容。") - .padding({ left: 30, right: 20 }) - .fontSize(12) - } - .width("100%") + Text($r('app.string.setting_privacy_desc_prefix', this.getPrivacyState())) + .padding({ left: 30, right: 20 }) + Row() { - Button("导出令牌密钥文件(*.atsf)") - .onClick((event: ClickEvent) => { + Button($r('app.string.setting_export')) + .onClick(() => { let saveOptions = new picker.DocumentSaveOptions() saveOptions.newFileNames = ["authenticator.atsf"] saveOptions.fileSuffixChoices = [".atsf"] @@ -170,17 +169,27 @@ export struct Setting { if (uris.length != 1) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "请选择一个文件保存路径" + message: $r('app.string.setting_export_pick_single') }) return } let file = fs.openSync(uris[0], fs.OpenMode.READ_WRITE) - let data = JSON.stringify(this.authDataStore.items) + let data = JSON.stringify(this.authDataStore.items, (key: string, value: string | number) => { + if (key == "password" || key == "tokenIconPath") { + return undefined; + } + // 使前面版本external的导出改变(如果打开过Detail页面,这里的external已经改变了) + if (key == "external" && value != "") { + let authdata = JSON.parse(value as string) as Record + return JSON.stringify(parseSteamExternalAuthData(authdata)) + } + return value; + }) fs.writeSync(file.fd, data) fs.close(file) this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "导出完成" + message: $r('app.string.setting_export_success') }) return }) @@ -190,7 +199,7 @@ export struct Setting { .alignItems(VerticalAlign.Center) Row() { - Text("令牌密钥文件包含 App 内所有令牌的密钥,文件内容没有加密,请小心保存以免泄露。") + Text($r('app.string.setting_export_desc')) .padding({ left: 30, right: 20 }) .fontSize(12) } @@ -198,12 +207,12 @@ export struct Setting { } Column() { - Text(`版本:${this.appInfo?.versionName}(${this.appInfo?.versionCode})`) + Text($r('app.string.setting_version_format', this.appInfo?.versionName, this.appInfo?.versionCode)) .margin(5) .copyOption(CopyOptions.LocalDevice) .fontSize(12) .fontWeight(FontWeight.Bold) - Text(`用户 QQ 群:931132608`) + Text($r('app.string.setting_qq_group_format', '931132608')) .margin(5) .copyOption(CopyOptions.LocalDevice) .fontSize(12) @@ -217,6 +226,6 @@ export struct Setting { .id("version") } } - .title("设置") + .title($r('app.string.setting_title')) } } \ No newline at end of file diff --git a/entry/src/main/ets/pages/Steam.ets b/entry/src/main/ets/pages/Steam.ets index 9e55d20..1ea3913 100644 --- a/entry/src/main/ets/pages/Steam.ets +++ b/entry/src/main/ets/pages/Steam.ets @@ -3,10 +3,11 @@ import { cryptoFramework } from '@kit.CryptoArchitectureKit' import { url, util } from '@kit.ArkTS' import { AsyncCallback, systemDateTime } from '@kit.BasicServicesKit' import * as steamapi from './steamapi/custom' -import * as steam2faapi from './steamapi/service_twofactor' import { EAuthSessionGuardType } from './steamapi/custom' +import * as steam2faapi from './steamapi/service_twofactor' import Long from 'long' -import { proxyServer, proxyAuthToken } from './proxy/Proxy' +import { proxyAuthToken, proxyServer } from './proxy/Proxy' +import { getLocalizedString } from '../entryability/EntryAbility' export class Confirmation { // Test = 1 @@ -268,7 +269,7 @@ export class SteamClient { const regexp = new RegExp("^https://s.team/q/(\\d+)/(\\d+)(\\?|$)"); let matchResult = regexp.exec(challengeURL) if (!matchResult || matchResult.length != 4) { - callback(new Error("无效的 Steam 登陆二维码")) + callback(new Error(getLocalizedString($r("app.string.invalid_steam_qr_code")))) return } let version = matchResult[1] diff --git a/entry/src/main/ets/pages/SteamConfirmationDetail.ets b/entry/src/main/ets/pages/SteamConfirmationDetail.ets index a493301..75c2600 100644 --- a/entry/src/main/ets/pages/SteamConfirmationDetail.ets +++ b/entry/src/main/ets/pages/SteamConfirmationDetail.ets @@ -1,5 +1,5 @@ -import { Confirmation } from "./Steam" -import { intl } from "@kit.LocalizationKit" +import { Confirmation } from './Steam' +import { intl } from '@kit.LocalizationKit' @Component export struct SteamConfirmationDetail { @@ -10,7 +10,7 @@ export struct SteamConfirmationDetail { Scroll() { Column() { Row() { - Text("标题") + Text($r('app.string.confirm_detail_headline')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.headline) @@ -22,7 +22,7 @@ export struct SteamConfirmationDetail { if (this.confirmation.summary.join(",").length > 0) { Row() { - Text("描述") + Text($r('app.string.confirm_detail_summary')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.summary.join(",")) @@ -34,7 +34,7 @@ export struct SteamConfirmationDetail { } Row() { - Text("创建时间") + Text($r('app.string.confirm_detail_created_at')) .width("30%") .textAlign(TextAlign.Start) Text((new Date(this.confirmation.creation_time * 1000)).toLocaleString((new intl.Locale()).toString(), @@ -46,7 +46,7 @@ export struct SteamConfirmationDetail { .alignItems(VerticalAlign.Center) Row() { - Text("创建者 ID") + Text($r('app.string.confirm_detail_creator_id')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.creator_id) @@ -57,7 +57,7 @@ export struct SteamConfirmationDetail { .alignItems(VerticalAlign.Center) Row() { - Text("类型编号") + Text($r('app.string.confirm_detail_type_id')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.type.toString()) @@ -68,7 +68,7 @@ export struct SteamConfirmationDetail { .alignItems(VerticalAlign.Center) Row() { - Text("类型名称") + Text($r('app.string.confirm_detail_type_name')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.type_name) @@ -79,7 +79,7 @@ export struct SteamConfirmationDetail { .alignItems(VerticalAlign.Center) Row() { - Text("确认 ID") + Text($r('app.string.confirm_detail_confirm_id')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.id) @@ -89,9 +89,8 @@ export struct SteamConfirmationDetail { .padding({ top: 5, bottom: 5 }) .alignItems(VerticalAlign.Center) - Row() { - Text("一次性编号") + Text($r('app.string.confirm_disposable_number')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.nonce) @@ -101,10 +100,9 @@ export struct SteamConfirmationDetail { .padding({ top: 5, bottom: 5 }) .alignItems(VerticalAlign.Center) - if (this.confirmation.icon && this.confirmation.icon.length > 0) { Row() { - Text("图标地址") + Text($r('app.string.confirm_detail_icon_url')) .width("30%") .textAlign(TextAlign.Start) Text(this.confirmation.icon) @@ -114,11 +112,9 @@ export struct SteamConfirmationDetail { .padding({ top: 5, bottom: 5 }) .alignItems(VerticalAlign.Center) } - - } } } - .title("确认详情") + .title($r('app.string.confirm_detail_title')) } } diff --git a/entry/src/main/ets/pages/SteamConfirmationList.ets b/entry/src/main/ets/pages/SteamConfirmationList.ets index a256037..26e9157 100644 --- a/entry/src/main/ets/pages/SteamConfirmationList.ets +++ b/entry/src/main/ets/pages/SteamConfirmationList.ets @@ -1,7 +1,7 @@ -import { DefaultToastDuration, SteamAuthData } from "./Base"; -import { Confirmation, SteamClient } from "./Steam"; -import { SymbolGlyphModifier } from "@kit.ArkUI"; -import { intl } from "@kit.LocalizationKit"; +import { DefaultToastDuration, SteamAuthData } from './Base'; +import { Confirmation, SteamClient } from './Steam'; +import { SymbolGlyphModifier } from '@kit.ArkUI'; +import { intl } from '@kit.LocalizationKit'; @Component export struct SteamConfirmationList { @@ -16,22 +16,22 @@ export struct SteamConfirmationList { this.loadSteamConfirmations() } - getType(conf: Confirmation): string { + getType(conf: Confirmation): ResourceStr { switch (conf.type) { case 1: - return "测试" + return $r('app.string.confirm_list_type_test') case 2: - return "交易" + return $r('app.string.confirm_list_type_trade') case 3: - return "出售" + return $r('app.string.confirm_list_type_sell') case 5: - return "修改手机号码" + return $r('app.string.confirm_list_type_change_phone') case 6: - return "解绑手机号码" + return $r('app.string.confirm_list_type_remove_phone') case 9: - return "创建访问密钥" + return $r('app.string.confirm_list_type_create_key') default: - return "未知类型" + return $r('app.string.confirm_list_type_unknown') } } @@ -55,14 +55,14 @@ export struct SteamConfirmationList { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `查询失败,错误:${err.message}` + message: $r('app.string.confirm_list_query_failed_format', err.message) }) return } this.steamConfirmations = info this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: `查询成功,共 ${info.length} 条待确认记录` + message: $r('app.string.confirm_list_query_success_format', info.length) }) }) } @@ -80,8 +80,8 @@ export struct SteamConfirmationList { allowAll() { this.getUIContext().getPromptAction().showActionMenu({ - title: "全部允许?", - buttons: [{ text: "是", color: $r("app.color.item_confirm_background_color") }], + title: $r('app.string.confirm_list_allow_all'), + buttons: [{ text: $r('app.string.common_yes'), color: $r("app.color.item_confirm_background_color") }], }).then((value) => { if (value.index == 0) { this.steamClient.allow(this.steamAuthData.steamID, this.steamAuthData.refreshToken, @@ -89,7 +89,7 @@ export struct SteamConfirmationList { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "确认请求失败,错误:${err.message}" + message: $r('app.string.confirm_list_allow_failed_format', err.message) }) return } @@ -101,8 +101,8 @@ export struct SteamConfirmationList { denyAll() { this.getUIContext().getPromptAction().showActionMenu({ - title: "全部拒绝?", - buttons: [{ text: "是", color: $r("app.color.item_delete_background_color") }], + title: $r('app.string.confirm_list_deny_all'), + buttons: [{ text: $r('app.string.common_yes'), color: $r("app.color.item_delete_background_color") }], }).then((value) => { if (value.index == 0) { this.steamClient.deny(this.steamAuthData.steamID, this.steamAuthData.refreshToken, @@ -110,7 +110,7 @@ export struct SteamConfirmationList { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "拒绝请求失败,错误:${err.message}" + message: $r('app.string.confirm_list_deny_failed_format', err.message) }) return } @@ -138,7 +138,7 @@ export struct SteamConfirmationList { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "确认请求失败,错误:${err.message}" + message: $r('app.string.confirm_list_allow_failed_format', err.message) }) return } @@ -161,7 +161,7 @@ export struct SteamConfirmationList { if (err) { this.getUIContext().getPromptAction().showToast({ duration: DefaultToastDuration, - message: "拒绝请求失败,错误:${err.message}" + message: $r('app.string.confirm_list_deny_failed_format', err.message) }) return } @@ -174,7 +174,7 @@ export struct SteamConfirmationList { build() { NavDestination() { if (this.loading) { - Text("查询中...") + Text($r('app.string.confirm_list_loading')) } else { List() { ForEach(this.steamConfirmations, (item: Confirmation) => { @@ -223,9 +223,8 @@ export struct SteamConfirmationList { } } } - .title("Steam 确认列表") + .title($r('app.string.confirm_list_title')) .menus([ - { value: "allow", isEnabled: !this.loading && this.steamConfirmations.length > 0, diff --git a/entry/src/main/ets/pages/proxy/Proxy.ets b/entry/src/main/ets/pages/proxy/Proxy.ets index 745c85e..905d870 100644 --- a/entry/src/main/ets/pages/proxy/Proxy.ets +++ b/entry/src/main/ets/pages/proxy/Proxy.ets @@ -1,2 +1,3 @@ export let proxyServer = "" + export let proxyAuthToken = "" diff --git a/entry/src/main/ets/utils/IconPackUtils.ets b/entry/src/main/ets/utils/IconPackUtils.ets new file mode 100644 index 0000000..fcf5eb4 --- /dev/null +++ b/entry/src/main/ets/utils/IconPackUtils.ets @@ -0,0 +1,383 @@ +import { fileIo as fs, picker } from '@kit.CoreFileKit'; +import { common } from '@kit.AbilityKit'; +import { BusinessError, zlib } from '@kit.BasicServicesKit'; +import { util } from '@kit.ArkTS'; + +export interface AegisIconDefinition { + filename: string; + name?: string; + category?: string; + issuer?: string[]; +} + +enum IconMatchType { + NORMAL = 0, + INVERSE = 1 +} + +export interface IconPackSection { + key: string; + title: string; + count: number; + getPathAt: (index: number) => string; +} + +export class AegisIconPack { + public invalid: boolean = false; + public onDevicePath: string = ''; + public uuid: string = ''; + public name: string = ''; + public version: number = 0; + public icons: AegisIconDefinition[] = []; + private iconMap: Map = new Map(); + + buildMap(): void { + this.iconMap.clear(); + this.icons.forEach((icon) => { + (icon.issuer ?? []).forEach((issuer) => { + this.iconMap.set(IconPackRegistry.normalizeIssuer(issuer), icon.filename); + }); + }); + } + + getSuggestedIconPaths(issuer: string): string[] { + if (!issuer) { + return []; + } + + const suggested: string[] = []; + this.icons.forEach((icon) => { + const matchType = IconPackRegistry.getMatchType(issuer, icon.issuer ?? []); + if (matchType === undefined) { + return; + } + + const path = `file://${this.onDevicePath}/${icon.filename}`; + if (matchType === IconMatchType.NORMAL) { + suggested.unshift(path); + } else { + suggested.push(path); + } + }); + return suggested; + } + + getIconPathByIssuer(issuer: string): string { + const suggested = this.getSuggestedIconPaths(issuer); + if (suggested.length > 0) { + return suggested[0]; + } + + const filename = this.iconMap.get(IconPackRegistry.normalizeIssuer(issuer)); + if (!filename) { + return ''; + } + return `file://${this.onDevicePath}/${filename}`; + } + + getIconPathAt(index: number): string { + if (index < 0 || index >= this.icons.length) { + return ''; + } + return `file://${this.onDevicePath}/${this.icons[index].filename}`; + } + + getAllIconPaths(): string[] { + return this.icons.map((icon) => `file://${this.onDevicePath}/${icon.filename}`); + } + + getKey(): string { + return this.onDevicePath; + } +} + +export class IconPackRegistry { + private static readonly DEFAULT_PACK_KEY = 'default'; + private static readonly DEFAULT_ISSUERS: string[] = ['aliyun', 'cloudflare', 'epic_games', 'github', 'gitlab', + 'google', 'huawei', 'jetbrains', 'microsoft', 'namecheap', 'neteasemail', 'nintendo', 'nvidia', 'steam', + 'twitter', 'ubisoft']; + + static normalizeIssuer(issuer: string): string { + return issuer.trim().toLowerCase().replaceAll(' ', '_'); + } + + static getDefaultPackKey(): string { + return IconPackRegistry.DEFAULT_PACK_KEY; + } + + static getIconPackDirectory(context: common.UIAbilityContext): string { + return `${context.filesDir}/icon_packs`; + } + + static ensureIconPackDirectory(context: common.UIAbilityContext): string { + const dir = IconPackRegistry.getIconPackDirectory(context); + if (!fs.accessSync(dir)) { + fs.mkdirSync(dir); + } + return dir; + } + + static getDefaultIconPathByIssuer(issuer: string): string { + const suggested = IconPackRegistry.getSuggestedDefaultIconPaths(issuer); + if (suggested.length > 0) { + return suggested[0]; + } + return ''; + } + + static getIconPathByIssuer(issuer: string, selectedPackKey: string, installedPacks: AegisIconPack[]): string { + if (!selectedPackKey || selectedPackKey === IconPackRegistry.DEFAULT_PACK_KEY) { + return IconPackRegistry.getDefaultIconPathByIssuer(issuer); + } + + const pack = installedPacks.find((item) => item.getKey() === selectedPackKey && !item.invalid); + if (!pack) { + return IconPackRegistry.getDefaultIconPathByIssuer(issuer); + } + + const path = pack.getIconPathByIssuer(issuer); + if (path) { + return path; + } + return IconPackRegistry.getDefaultIconPathByIssuer(issuer); + } + + static getDefaultIconPaths(): string[] { + return IconPackRegistry.DEFAULT_ISSUERS.map((issuer) => `rawfile://icons/${issuer}.png`); + } + + static getSuggestedDefaultIconPaths(issuer: string): string[] { + if (!issuer) { + return []; + } + + const suggested: string[] = []; + IconPackRegistry.DEFAULT_ISSUERS.forEach((candidate) => { + const matchType = IconPackRegistry.getMatchType(issuer, [candidate, candidate.replaceAll('_', ' ')]); + if (matchType === undefined) { + return; + } + + const path = `rawfile://icons/${candidate}.png`; + if (matchType === IconMatchType.NORMAL) { + suggested.unshift(path); + } else { + suggested.push(path); + } + }); + return suggested; + } + + static getSuggestedIconPaths(issuer: string, installedPacks: AegisIconPack[]): string[] { + const suggested: string[] = []; + const seen = new Set(); + const pushPath = (path: string) => { + if (!path || seen.has(path)) { + return; + } + seen.add(path); + suggested.push(path); + }; + + IconPackRegistry.getSuggestedDefaultIconPaths(issuer).forEach(pushPath); + installedPacks.filter((pack) => !pack.invalid) + .forEach((pack) => pack.getSuggestedIconPaths(issuer).forEach(pushPath)); + return suggested; + } + + static getIconSections(installedPacks: AegisIconPack[]): IconPackSection[] { + const sections: IconPackSection[] = [ + { + key: IconPackRegistry.DEFAULT_PACK_KEY, + title: 'builtin', + count: IconPackRegistry.DEFAULT_ISSUERS.length, + getPathAt: (index: number): string => { + if (index < 0 || index >= IconPackRegistry.DEFAULT_ISSUERS.length) { + return ''; + } + return `rawfile://icons/${IconPackRegistry.DEFAULT_ISSUERS[index]}.png`; + } + } + ]; + + installedPacks.filter((pack) => !pack.invalid).forEach((pack) => { + sections.push({ + key: pack.getKey(), + title: pack.name, + count: pack.icons.length, + getPathAt: (index: number): string => pack.getIconPathAt(index) + }); + }); + + return sections; + } + + static getMatchType(issuer: string, candidates: string[]): IconMatchType | undefined { + const normalizedIssuer = issuer.trim().toLowerCase(); + if (!normalizedIssuer) { + return undefined; + } + + let inverseMatch = false; + for (const candidate of candidates) { + const normalizedCandidate = candidate.trim().toLowerCase(); + if (!normalizedCandidate) { + continue; + } + if (normalizedCandidate.includes(normalizedIssuer)) { + return IconMatchType.NORMAL; + } + if (normalizedIssuer.includes(normalizedCandidate)) { + inverseMatch = true; + } + } + + if (inverseMatch) { + return IconMatchType.INVERSE; + } + return undefined; + } + + static async loadInstalledPacks(context: common.UIAbilityContext): Promise { + const dir = IconPackRegistry.ensureIconPackDirectory(context); + let folders: string[] = []; + try { + folders = await fs.listFile(dir); + } catch (_) { + return []; + } + + const packs: AegisIconPack[] = []; + for (const folder of folders) { + const packPath = `${dir}/${folder}`; + const pack = await IconPackRegistry.loadSinglePack(packPath); + if (pack) { + packs.push(pack); + } + } + packs.sort((left, right) => left.name.localeCompare(right.name)); + return packs; + } + + private static async loadSinglePack(packPath: string): Promise { + const packJsonPath = `${packPath}/pack.json`; + if (!fs.accessSync(packJsonPath)) { + const invalidPack = new AegisIconPack(); + invalidPack.invalid = true; + invalidPack.onDevicePath = packPath; + invalidPack.name = packPath.substring(packPath.lastIndexOf('/') + 1); + return invalidPack; + } + + try { + const content = await IconPackRegistry.readTextFile(packJsonPath); + const parsed = JSON.parse(content) as AegisIconPack; + const pack = new AegisIconPack(); + pack.onDevicePath = packPath; + pack.uuid = parsed.uuid ?? ''; + pack.name = parsed.name ?? packPath.substring(packPath.lastIndexOf('/') + 1); + pack.version = parsed.version ?? 0; + pack.icons = parsed.icons ?? []; + pack.buildMap(); + return pack; + } catch (_) { + const invalidPack = new AegisIconPack(); + invalidPack.invalid = true; + invalidPack.onDevicePath = packPath; + invalidPack.name = packPath.substring(packPath.lastIndexOf('/') + 1); + return invalidPack; + } + } + + static async importFromZip(context: common.UIAbilityContext, zipUri: string): Promise { + const dir = IconPackRegistry.ensureIconPackDirectory(context); + const zipFile = fs.openSync(zipUri, fs.OpenMode.READ_ONLY); + const zipPath = zipFile.path; + const zipName = zipPath.substring(zipPath.lastIndexOf('/') + 1); + await fs.close(zipFile.fd); + + const baseName = zipName.endsWith('.zip') ? zipName.substring(0, zipName.length - 4) : zipName; + let targetDir = `${dir}/${baseName}`; + let suffix = 1; + while (fs.accessSync(targetDir)) { + targetDir = `${dir}/${baseName}_${suffix++}`; + } + + fs.mkdirSync(targetDir); + try { + await zlib.decompressFile(zipPath, targetDir); + } catch (error) { + IconPackRegistry.safeRemoveDir(targetDir); + throw new Error(IconPackRegistry.formatImportError(error as Object)); + } + + const pack = await IconPackRegistry.loadSinglePack(targetDir); + if (!pack || pack.invalid) { + IconPackRegistry.safeRemoveDir(targetDir); + throw new Error('Invalid icon pack'); + } + } + + static async pickZipFiles(context: common.UIAbilityContext): Promise { + const options = new picker.DocumentSelectOptions(); + options.maxSelectNumber = 5; + options.fileSuffixFilters = ['.zip']; + const viewPicker = new picker.DocumentViewPicker(context); + return viewPicker.select(options); + } + + static async removePack(pack: AegisIconPack): Promise { + if (!pack.onDevicePath) { + return; + } + try { + await fs.rmdir(pack.onDevicePath); + } catch (_) { + IconPackRegistry.safeRemoveDir(pack.onDevicePath); + } + } + + private static safeRemoveDir(path: string): void { + if (!fs.accessSync(path)) { + return; + } + try { + fs.rmdirSync(path); + } catch (_) { + try { + const files = fs.listFileSync(path); + files.forEach((name) => { + const child = `${path}/${name}`; + try { + fs.unlinkSync(child); + } catch (_) { + IconPackRegistry.safeRemoveDir(child); + } + }); + fs.rmdirSync(path); + } catch (_) { + } + } + } + + private static async readTextFile(uri: string): Promise { + const file = await fs.open(uri, fs.OpenMode.READ_ONLY); + try { + const stat = fs.statSync(file.fd); + const buffer = new ArrayBuffer(stat.size); + const readBytes = fs.readSync(file.fd, buffer); + return new util.TextDecoder().decodeToString(new Uint8Array(buffer.slice(0, readBytes))); + } finally { + await fs.close(file.fd); + } + } + + static formatImportError(error: Object): string { + const businessError = error as BusinessError; + if (businessError && businessError.message) { + return businessError.message; + } + return `${error}`; + } +} + diff --git a/entry/src/main/resources/base/element/color.json b/entry/src/main/resources/base/element/color.json index 00eb40f..74eb409 100644 --- a/entry/src/main/resources/base/element/color.json +++ b/entry/src/main/resources/base/element/color.json @@ -31,6 +31,14 @@ { "name": "start_window_background", "value": "#F1F1F1" + }, + { + "name": "icon_pack_row_background", + "value": "#f5f5f5" + }, + { + "name": "icon_picker_background", + "value": "#cccccc" } ] } \ No newline at end of file diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index 4c761d3..50bd43a 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -23,6 +23,702 @@ { "name": "EntryAbility_label", "value": "KDAuth" + }, + { + "name": "common_cancel", + "value": "Cancel" + }, + { + "name": "common_confirm", + "value": "Confirm" + }, + { + "name": "common_add", + "value": "Add" + }, + { + "name": "common_login", + "value": "Login" + }, + { + "name": "common_yes", + "value": "Yes" + }, + { + "name": "common_safe_mode_unlock_first", + "value": "Safe mode is enabled. Please unlock first." + }, + { + "name": "common_enter_token_first", + "value": "Please enter the token first." + }, + { + "name": "common_enter_code_first", + "value": "Please enter the verification code first." + }, + { + "name": "base_unknown_file_format", + "value": "Unknown file format." + }, + { + "name": "base_invalid_scheme_format", + "value": "Invalid scheme: %s" + }, + { + "name": "base_recover_item_missing_format", + "value": "Token secret #%d is missing %s" + }, + { + "name": "base_missing_format", + "value": "Missing %s" + }, + { + "name": "add_title", + "value": "Add" + }, + { + "name": "add_totp_secret_title", + "value": "Add via TOTP secret" + }, + { + "name": "add_totp_secret_desc", + "value": "A TOTP secret is a string made of A-Z and 2-7, for example: ABCDEFGHIJ234567" + }, + { + "name": "add_totp_uri_title", + "value": "Add via TOTP URI" + }, + { + "name": "add_totp_uri_desc", + "value": "A TOTP URI starts with otpauth://totp/, for example: otpauth://totp/Example:alic e@example.com?secret=ABCDEFGHIJ234567&issuer=Example" + }, + { + "name": "add_restore_title", + "value": "Restore from token secret file (*.atsf)" + }, + { + "name": "add_restore_desc", + "value": "The token secret file (*.atsf) can be exported from Settings. Editing the file may make restore fail. Restore does not overwrite existing tokens, so remove duplicates manually if needed." + }, + { + "name": "add_restore_pick_single", + "value": "Please select one token secret file." + }, + { + "name": "add_restore_success", + "value": "Restore completed." + }, + { + "name": "add_restore_error_format", + "value": "Restore failed: %s" + }, + { + "name": "add_steam_title", + "value": "Add Steam token" + }, + { + "name": "add_steam_desc", + "value": "Supports 5-character tokens, QR login, and trade confirmations." + }, + { + "name": "add_secret_title", + "value": "Add via TOTP secret" + }, + { + "name": "add_secret_provider", + "value": "Provider" + }, + { + "name": "add_secret_provider_placeholder", + "value": "Enter provider name" + }, + { + "name": "add_secret_account", + "value": "Account" + }, + { + "name": "add_secret_account_placeholder", + "value": "Username or email" + }, + { + "name": "add_secret_secret", + "value": "Secret" + }, + { + "name": "add_secret_secret_placeholder", + "value": "Characters A-Z and 2-7 only" + }, + { + "name": "add_secret_digits", + "value": "Digits" + }, + { + "name": "add_secret_invalid_secret", + "value": "The secret must contain only A-Z and 2-7, with at least 2 characters." + }, + { + "name": "add_secret_error_format", + "value": "TOTP error: %s" + }, + { + "name": "add_uri_title", + "value": "Add via TOTP URI" + }, + { + "name": "add_uri_heading", + "value": "Enter TOTP URI" + }, + { + "name": "add_uri_desc", + "value": "If you need to add multiple entries, enter one URI per line." + }, + { + "name": "add_uri_partial_error_format", + "value": "Added %d item(s). The TOTP URI on line %d is invalid: %s" + }, + { + "name": "add_steam_secret_title", + "value": "Add Steam token" + }, + { + "name": "index_auth_title", + "value": "Please authenticate" + }, + { + "name": "index_empty", + "value": "No token has been added yet.\nTap the add or scan button in the top-right corner to add one." + }, + { + "name": "index_add_success", + "value": "Token added successfully." + }, + { + "name": "index_no_steam_token", + "value": "No Steam token found. Scan login to Steam is unavailable. Please add a Steam token first." + }, + { + "name": "index_no_steam_auth", + "value": "The Steam token has no authentication info set. Scan login to Steam is unavailable. Please configure it in the token details page." + }, + { + "name": "index_go_to_steam_detail", + "value": "Please scan to log in on the Steam token details page." + }, + { + "name": "detail_steam_input_title", + "value": "Enter Steam auth info" + }, + { + "name": "detail_steam_input_desc", + "value": "After authentication is configured, you can use Steam QR login and trade confirmations." + }, + { + "name": "detail_steam_input_error_format", + "value": "Invalid auth info format. Please make sure the content comes from a maFile: %s" + }, + { + "name": "detail_steam_invalid_error_format", + "value": "Invalid auth info format: %s, content: %s" + }, + { + "name": "detail_steam_login_failed_format", + "value": "Steam login failed: %s" + }, + { + "name": "detail_steam_login_success", + "value": "Steam login succeeded." + }, + { + "name": "detail_steam_login_start", + "value": "Starting Steam login..." + }, + { + "name": "detail_steam_section_title", + "value": "----- Steam Authentication -----" + }, + { + "name": "detail_steam_section_desc", + "value": "If QR login or trade confirmations time out or fail, enable Enhanced Network in Settings." + }, + { + "name": "detail_steam_account", + "value": "Steam account" + }, + { + "name": "detail_steam_id", + "value": "Steam ID" + }, + { + "name": "detail_device_id", + "value": "Device ID" + }, + { + "name": "detail_expire_time", + "value": "Expiration" + }, + { + "name": "detail_reset", + "value": "Reset" + }, + { + "name": "detail_update_expire", + "value": "Refresh expiry" + }, + { + "name": "detail_setup_steam", + "value": "Set up Steam auth info" + }, + { + "name": "detail_alias", + "value": "Alias" + }, + { + "name": "detail_account", + "value": "Account" + }, + { + "name": "detail_issuer", + "value": "Issuer" + }, + { + "name": "detail_digits", + "value": "Digits" + }, + { + "name": "setting_title", + "value": "Settings" + }, + { + "name": "setting_auto_dark", + "value": "Follow system dark mode" + }, + { + "name": "setting_auto_dark_desc", + "value": "When enabled, the app follows the system appearance automatically." + }, + { + "name": "setting_safe_mode", + "value": "Enable safe mode" + }, + { + "name": "setting_safe_mode_desc", + "value": "When enabled, opening the app requires biometric or PIN authentication before viewing tokens. It takes effect the next time the app starts." + }, + { + "name": "setting_hide_token", + "value": "Hide tokens" + }, + { + "name": "setting_hide_token_desc", + "value": "When enabled, tokens on the home page are hidden by default. Tap to reveal them. The details page is not affected." + }, + { + "name": "icon_pack_title", + "value": "Icon packs" + }, + { + "name": "icon_pack_desc", + "value": "Use built-in issuer icons or import Aegis-compatible zip packs." + }, + { + "name": "icon_pack_manage", + "value": "Manage" + }, + { + "name": "icon_pack_hide", + "value": "Hide" + }, + { + "name": "icon_pack_builtin", + "value": "Built-in" + }, + { + "name": "icon_pack_builtin_desc", + "value": "Default issuer icon library" + }, + { + "name": "icon_pack_import", + "value": "Import zip" + }, + { + "name": "icon_pack_remove", + "value": "Remove" + }, + { + "name": "icon_pack_import_success", + "value": "Icon pack imported" + }, + { + "name": "icon_pack_import_failed_format", + "value": "Import failed: %s" + }, + { + "name": "icon_pack_remove_success", + "value": "Icon pack removed" + }, + { + "name": "icon_pack_remove_failed_format", + "value": "Remove failed: %s" + }, + { + "name": "icon_pack_installed_count_format", + "value": "%d installed" + }, + { + "name": "icon_pack_invalid_name_format", + "value": "%s (invalid)" + }, + { + "name": "icon_pack_invalid_desc", + "value": "pack.json missing or invalid" + }, + { + "name": "icon_pack_meta_format", + "value": "%d icons · v%s" + }, + { + "name": "setting_network", + "value": "Enhanced network" + }, + { + "name": "setting_network_enabled", + "value": "Developer mode enabled." + }, + { + "name": "setting_network_disabled", + "value": "Developer mode disabled." + }, + { + "name": "setting_network_desc", + "value": "When enabled, improves access stability for token-related network services." + }, + { + "name": "setting_privacy_policy", + "value": "Privacy policy" + }, + { + "name": "setting_privacy_agreed", + "value": "Agreed" + }, + { + "name": "setting_privacy_disagreed", + "value": "Not agreed" + }, + { + "name": "setting_privacy_desc_prefix", + "value": "All features are available regardless of whether you agree to the privacy policy. Tap the [%s] on the right to review it again." + }, + { + "name": "setting_export", + "value": "Export token secret file (.atsf)" + }, + { + "name": "setting_export_pick_single", + "value": "Please choose exactly one file path." + }, + { + "name": "setting_export_success", + "value": "Export completed." + }, + { + "name": "setting_export_desc", + "value": "The token secret file contains all token secrets stored in the app. The file is not encrypted, so please keep it safe." + }, + { + "name": "setting_version_format", + "value": "Version: %s(%d)" + }, + { + "name": "setting_qq_group_format", + "value": "User QQ group: %s" + }, + { + "name": "privacy_title", + "value": "Privacy Policy" + }, + { + "name": "privacy_content", + "value": "更新日期:2025/04/03\n生效日期:2025/04/03\n导言\n千维验证器是一款由 郭维 (以下简称“我们”)提供的产品。 您在使用我们的服务时,我们不会收集和使用您的相关信息,我们希望通过本《隐私政策》向您说明。本《隐私政策》与您所使用的千维验证器服务息息相关,希望您仔细阅读,在需要时,按照本《隐私政策》的指引,作出您认为适当的选择。本《隐私政策》中涉及的相关技术词汇,我们尽量以简明扼要的表述,并提供进一步说明的链接,以便您的理解。\n\n您使用或继续使用我们的服务,即意味着同意本《隐私政策》。\n\n感谢您使用我们的服务!我们重视您的隐私,并承诺保护您的个人信息。在使用本服务时,我们希望向您明确声明,我们不会主动收集、存储或共享任何个人信息。请您在使用本服务前,仔细阅读本隐私政策。\n\n1. 我们不收集个人信息\n在您使用本服务的过程中,我们不会主动收集您的任何个人信息。无论您是匿名使用服务,还是以其他方式与我们进行互动,我们都不会要求或获取您的姓名、电子邮件地址、电话号码、住址、支付信息等个人身份信息。\n\n2. 不涉及第三方数据共享\n由于我们不收集任何个人信息,我们也不会将您的任何数据与第三方共享、出售或租赁。您的隐私安全对我们来说至关重要,我们不会向任何第三方提供与您身份相关的任何数据。\n\n3. 技术数据收集\n尽管我们不会收集您的个人信息,但我们可能会使用一些常见的技术手段来收集非个人信息,例如您的设备类型、浏览器类型、操作系统版本、访问日期和时间等。这些信息仅用于改善服务质量和用户体验,不会用于识别您个人身份。\n\n4. 数据存储\n由于我们不会收集个人信息,因此不会在服务器上存储任何与您身份相关的个人数据。任何与服务有关的日志数据都仅用于技术分析和系统维护,且不会用于任何身份识别。\n\n5. 安全性\n虽然我们不收集您的个人信息,但我们依然采取合理的安全措施来保护我们服务的数据安全。这些措施包括但不限于加密技术、服务器安全和网络防护,以确保我们的系统免受外部攻击或数据泄露。\n\n6. 儿童隐私\n我们的服务并不针对儿童,我们不会 knowingly 收集或请求未满18岁用户的个人信息。如果我们发现收集了未满18岁用户的个人信息,我们将尽快删除相关数据。\n\n7. 政策变更\n我们可能会根据法律、政策或服务改进的需要,适时更新或修改本隐私政策。如果本隐私政策发生重要变更,我们将在适当的地方进行通知,以确保您及时了解我们的隐私保护措施。\n\n8. 联系我们\n如果您对本隐私政策有任何疑问或需要更多信息,请随时联系我们。" + }, + { + "name": "privacy_disagree", + "value": "Disagree" + }, + { + "name": "privacy_agree", + "value": "Agree" + }, + { + "name": "confirm_detail_title", + "value": "Confirmation details" + }, + { + "name": "confirm_detail_headline", + "value": "Title" + }, + { + "name": "confirm_detail_summary", + "value": "Summary" + }, + { + "name": "confirm_detail_created_at", + "value": "Created at" + }, + { + "name": "confirm_detail_creator_id", + "value": "Creator ID" + }, + { + "name": "confirm_detail_type_id", + "value": "Type ID" + }, + { + "name": "confirm_detail_type_name", + "value": "Type name" + }, + { + "name": "confirm_detail_confirm_id", + "value": "Confirmation ID" + }, + { + "name": "confirm_disposable_number", + "value": "Disposable number" + }, + { + "name": "confirm_detail_icon_url", + "value": "Icon URL" + }, + { + "name": "confirm_list_title", + "value": "Steam confirmations" + }, + { + "name": "confirm_list_type_test", + "value": "Test" + }, + { + "name": "confirm_list_type_trade", + "value": "Trade" + }, + { + "name": "confirm_list_type_sell", + "value": "Sell" + }, + { + "name": "confirm_list_type_change_phone", + "value": "Change phone number" + }, + { + "name": "confirm_list_type_remove_phone", + "value": "Remove phone number" + }, + { + "name": "confirm_list_type_create_key", + "value": "Create access key" + }, + { + "name": "confirm_list_type_unknown", + "value": "Unknown type" + }, + { + "name": "confirm_list_query_failed_format", + "value": "Query failed: %s" + }, + { + "name": "confirm_list_query_success_format", + "value": "Query succeeded. Pending items: %d" + }, + { + "name": "confirm_list_allow_all", + "value": "Allow all?" + }, + { + "name": "confirm_list_deny_all", + "value": "Deny all?" + }, + { + "name": "confirm_list_allow_failed_format", + "value": "Allow request failed: %s" + }, + { + "name": "confirm_list_deny_failed_format", + "value": "Deny request failed: %s" + }, + { + "name": "confirm_list_loading", + "value": "Querying..." + }, + { + "name": "steam_intro_title", + "value": "How it works" + }, + { + "name": "steam_intro_steps", + "value": "1. Sign in to Steam\n2. Add an authenticator to Steam" + }, + { + "name": "steam_intro_note", + "value": "Note: You will need to enter your Steam password and verification code during the process. If requests time out or fail, enable Enhanced Network in Settings." + }, + { + "name": "steam_start_now", + "value": "Start now" + }, + { + "name": "steam_login_title", + "value": "Sign in to Steam" + }, + { + "name": "steam_login_prompt", + "value": "Enter your Steam account name and password" + }, + { + "name": "steam_account_name", + "value": "Account name" + }, + { + "name": "steam_password", + "value": "Password" + }, + { + "name": "steam_login_fetching_auth", + "value": "Steam login succeeded. Fetching authentication info..." + }, + { + "name": "steam_auth_fetch_failed_format", + "value": "Failed to fetch Steam authentication info: %s" + }, + { + "name": "steam_mfa_device_confirmation", + "value": "Steam has multi-factor authentication enabled. Confirm this sign-in on another device, then tap Complete sign-in." + }, + { + "name": "steam_mfa_email_confirmation", + "value": "Steam has multi-factor authentication enabled. A confirmation email has been sent. Tap the confirmation link in the email, then tap Complete sign-in." + }, + { + "name": "steam_mfa_device_code", + "value": "Steam has multi-factor authentication enabled. Get the token from your bound authenticator and enter it below." + }, + { + "name": "steam_mfa_email_code", + "value": "Steam has multi-factor authentication enabled. The token was sent by email. Enter it below." + }, + { + "name": "steam_complete_login", + "value": "Complete sign-in" + }, + { + "name": "steam_fetching_auth", + "value": "Fetching Steam authentication info..." + }, + { + "name": "steam_link_success_format", + "value": "Signed in successfully as %s. Tap Add to add Authenticator to Steam." + }, + { + "name": "steam_add_authenticator_failed_format", + "value": "Failed to add Authenticator: %s" + }, + { + "name": "steam_confirm_link_sms", + "value": "Steam is adding an authenticator. Enter the SMS verification code." + }, + { + "name": "steam_confirm_link_email", + "value": "Steam is adding an authenticator. The verification code was sent by email. Enter the code." + }, + { + "name": "steam_complete_add", + "value": "Complete add" + }, + { + "name": "steam_finalize_failed_format", + "value": "Failed to add authenticator: %s" + }, + { + "name": "steam_token_mismatch", + "value": "The generated token does not match Steam. Tap Complete add once more." + }, + { + "name": "steam_add_success", + "value": "Added successfully." + }, + { + "name": "steam_transfer_prompt", + "value": "Another authenticator is already added. Do you want to transfer it?" + }, + { + "name": "steam_transfer_note", + "value": "Important: Make sure the Steam account is already linked to a phone number, otherwise transfer will fail." + }, + { + "name": "steam_transfer_button", + "value": "Transfer" + }, + { + "name": "steam_transfer_failed_format", + "value": "Failed to transfer authenticator: %s" + }, + { + "name": "steam_transfer_sms", + "value": "Steam is transferring the authenticator. Enter the SMS verification code." + }, + { + "name": "steam_complete_transfer", + "value": "Complete transfer" + }, + { + "name": "steam_refresh_title", + "value": "Refresh Steam auth expiry" + }, + { + "name": "steam_refresh_prompt", + "value": "Refreshing requires signing in to Steam again. Enter your password." + }, + { + "name": "steam_refresh_confirmation_type_error_format", + "value": "Steam login succeeded, but the confirmation type is invalid: %d" + }, + { + "name": "steam_refresh_submitting", + "value": "Steam login succeeded. Submitting token..." + }, + { + "name": "steam_refresh_submit_failed_format", + "value": "Failed to submit Steam token: %s" + }, + { + "name": "steam_refresh_submit_success", + "value": "Steam token submitted successfully. Fetching authentication info..." + }, + { + "name": "steam_refresh_success", + "value": "Steam authentication info updated successfully." + }, + { + "name": "common_scan_failed_format", + "value": "Scan failed: %s" + }, + { + "name": "invalid_steam_qr_code", + "value": "Invalid Steam login QR code." + }, + { + "name": "icon_picker_auto", + "value": "Auto" + }, + { + "name": "icon_picker_recommended", + "value": "Recommended" + }, + { + "name": "icon_picker_title", + "value": "Choose icon" + }, + { + "name": "icon_picker_mode", + "value": "Display mode" } ] } \ No newline at end of file diff --git a/entry/src/main/resources/dark/element/color.json b/entry/src/main/resources/dark/element/color.json index 86c3edd..6c8fd40 100644 --- a/entry/src/main/resources/dark/element/color.json +++ b/entry/src/main/resources/dark/element/color.json @@ -31,6 +31,14 @@ { "name": "start_window_background", "value": "#000000" + }, + { + "name": "icon_pack_row_background", + "value": "#191919" + }, + { + "name": "icon_picker_background", + "value": "#000000" } ] } diff --git a/entry/src/main/resources/en_US/element/string.json b/entry/src/main/resources/en_US/element/string.json index 4c761d3..50bd43a 100644 --- a/entry/src/main/resources/en_US/element/string.json +++ b/entry/src/main/resources/en_US/element/string.json @@ -23,6 +23,702 @@ { "name": "EntryAbility_label", "value": "KDAuth" + }, + { + "name": "common_cancel", + "value": "Cancel" + }, + { + "name": "common_confirm", + "value": "Confirm" + }, + { + "name": "common_add", + "value": "Add" + }, + { + "name": "common_login", + "value": "Login" + }, + { + "name": "common_yes", + "value": "Yes" + }, + { + "name": "common_safe_mode_unlock_first", + "value": "Safe mode is enabled. Please unlock first." + }, + { + "name": "common_enter_token_first", + "value": "Please enter the token first." + }, + { + "name": "common_enter_code_first", + "value": "Please enter the verification code first." + }, + { + "name": "base_unknown_file_format", + "value": "Unknown file format." + }, + { + "name": "base_invalid_scheme_format", + "value": "Invalid scheme: %s" + }, + { + "name": "base_recover_item_missing_format", + "value": "Token secret #%d is missing %s" + }, + { + "name": "base_missing_format", + "value": "Missing %s" + }, + { + "name": "add_title", + "value": "Add" + }, + { + "name": "add_totp_secret_title", + "value": "Add via TOTP secret" + }, + { + "name": "add_totp_secret_desc", + "value": "A TOTP secret is a string made of A-Z and 2-7, for example: ABCDEFGHIJ234567" + }, + { + "name": "add_totp_uri_title", + "value": "Add via TOTP URI" + }, + { + "name": "add_totp_uri_desc", + "value": "A TOTP URI starts with otpauth://totp/, for example: otpauth://totp/Example:alic e@example.com?secret=ABCDEFGHIJ234567&issuer=Example" + }, + { + "name": "add_restore_title", + "value": "Restore from token secret file (*.atsf)" + }, + { + "name": "add_restore_desc", + "value": "The token secret file (*.atsf) can be exported from Settings. Editing the file may make restore fail. Restore does not overwrite existing tokens, so remove duplicates manually if needed." + }, + { + "name": "add_restore_pick_single", + "value": "Please select one token secret file." + }, + { + "name": "add_restore_success", + "value": "Restore completed." + }, + { + "name": "add_restore_error_format", + "value": "Restore failed: %s" + }, + { + "name": "add_steam_title", + "value": "Add Steam token" + }, + { + "name": "add_steam_desc", + "value": "Supports 5-character tokens, QR login, and trade confirmations." + }, + { + "name": "add_secret_title", + "value": "Add via TOTP secret" + }, + { + "name": "add_secret_provider", + "value": "Provider" + }, + { + "name": "add_secret_provider_placeholder", + "value": "Enter provider name" + }, + { + "name": "add_secret_account", + "value": "Account" + }, + { + "name": "add_secret_account_placeholder", + "value": "Username or email" + }, + { + "name": "add_secret_secret", + "value": "Secret" + }, + { + "name": "add_secret_secret_placeholder", + "value": "Characters A-Z and 2-7 only" + }, + { + "name": "add_secret_digits", + "value": "Digits" + }, + { + "name": "add_secret_invalid_secret", + "value": "The secret must contain only A-Z and 2-7, with at least 2 characters." + }, + { + "name": "add_secret_error_format", + "value": "TOTP error: %s" + }, + { + "name": "add_uri_title", + "value": "Add via TOTP URI" + }, + { + "name": "add_uri_heading", + "value": "Enter TOTP URI" + }, + { + "name": "add_uri_desc", + "value": "If you need to add multiple entries, enter one URI per line." + }, + { + "name": "add_uri_partial_error_format", + "value": "Added %d item(s). The TOTP URI on line %d is invalid: %s" + }, + { + "name": "add_steam_secret_title", + "value": "Add Steam token" + }, + { + "name": "index_auth_title", + "value": "Please authenticate" + }, + { + "name": "index_empty", + "value": "No token has been added yet.\nTap the add or scan button in the top-right corner to add one." + }, + { + "name": "index_add_success", + "value": "Token added successfully." + }, + { + "name": "index_no_steam_token", + "value": "No Steam token found. Scan login to Steam is unavailable. Please add a Steam token first." + }, + { + "name": "index_no_steam_auth", + "value": "The Steam token has no authentication info set. Scan login to Steam is unavailable. Please configure it in the token details page." + }, + { + "name": "index_go_to_steam_detail", + "value": "Please scan to log in on the Steam token details page." + }, + { + "name": "detail_steam_input_title", + "value": "Enter Steam auth info" + }, + { + "name": "detail_steam_input_desc", + "value": "After authentication is configured, you can use Steam QR login and trade confirmations." + }, + { + "name": "detail_steam_input_error_format", + "value": "Invalid auth info format. Please make sure the content comes from a maFile: %s" + }, + { + "name": "detail_steam_invalid_error_format", + "value": "Invalid auth info format: %s, content: %s" + }, + { + "name": "detail_steam_login_failed_format", + "value": "Steam login failed: %s" + }, + { + "name": "detail_steam_login_success", + "value": "Steam login succeeded." + }, + { + "name": "detail_steam_login_start", + "value": "Starting Steam login..." + }, + { + "name": "detail_steam_section_title", + "value": "----- Steam Authentication -----" + }, + { + "name": "detail_steam_section_desc", + "value": "If QR login or trade confirmations time out or fail, enable Enhanced Network in Settings." + }, + { + "name": "detail_steam_account", + "value": "Steam account" + }, + { + "name": "detail_steam_id", + "value": "Steam ID" + }, + { + "name": "detail_device_id", + "value": "Device ID" + }, + { + "name": "detail_expire_time", + "value": "Expiration" + }, + { + "name": "detail_reset", + "value": "Reset" + }, + { + "name": "detail_update_expire", + "value": "Refresh expiry" + }, + { + "name": "detail_setup_steam", + "value": "Set up Steam auth info" + }, + { + "name": "detail_alias", + "value": "Alias" + }, + { + "name": "detail_account", + "value": "Account" + }, + { + "name": "detail_issuer", + "value": "Issuer" + }, + { + "name": "detail_digits", + "value": "Digits" + }, + { + "name": "setting_title", + "value": "Settings" + }, + { + "name": "setting_auto_dark", + "value": "Follow system dark mode" + }, + { + "name": "setting_auto_dark_desc", + "value": "When enabled, the app follows the system appearance automatically." + }, + { + "name": "setting_safe_mode", + "value": "Enable safe mode" + }, + { + "name": "setting_safe_mode_desc", + "value": "When enabled, opening the app requires biometric or PIN authentication before viewing tokens. It takes effect the next time the app starts." + }, + { + "name": "setting_hide_token", + "value": "Hide tokens" + }, + { + "name": "setting_hide_token_desc", + "value": "When enabled, tokens on the home page are hidden by default. Tap to reveal them. The details page is not affected." + }, + { + "name": "icon_pack_title", + "value": "Icon packs" + }, + { + "name": "icon_pack_desc", + "value": "Use built-in issuer icons or import Aegis-compatible zip packs." + }, + { + "name": "icon_pack_manage", + "value": "Manage" + }, + { + "name": "icon_pack_hide", + "value": "Hide" + }, + { + "name": "icon_pack_builtin", + "value": "Built-in" + }, + { + "name": "icon_pack_builtin_desc", + "value": "Default issuer icon library" + }, + { + "name": "icon_pack_import", + "value": "Import zip" + }, + { + "name": "icon_pack_remove", + "value": "Remove" + }, + { + "name": "icon_pack_import_success", + "value": "Icon pack imported" + }, + { + "name": "icon_pack_import_failed_format", + "value": "Import failed: %s" + }, + { + "name": "icon_pack_remove_success", + "value": "Icon pack removed" + }, + { + "name": "icon_pack_remove_failed_format", + "value": "Remove failed: %s" + }, + { + "name": "icon_pack_installed_count_format", + "value": "%d installed" + }, + { + "name": "icon_pack_invalid_name_format", + "value": "%s (invalid)" + }, + { + "name": "icon_pack_invalid_desc", + "value": "pack.json missing or invalid" + }, + { + "name": "icon_pack_meta_format", + "value": "%d icons · v%s" + }, + { + "name": "setting_network", + "value": "Enhanced network" + }, + { + "name": "setting_network_enabled", + "value": "Developer mode enabled." + }, + { + "name": "setting_network_disabled", + "value": "Developer mode disabled." + }, + { + "name": "setting_network_desc", + "value": "When enabled, improves access stability for token-related network services." + }, + { + "name": "setting_privacy_policy", + "value": "Privacy policy" + }, + { + "name": "setting_privacy_agreed", + "value": "Agreed" + }, + { + "name": "setting_privacy_disagreed", + "value": "Not agreed" + }, + { + "name": "setting_privacy_desc_prefix", + "value": "All features are available regardless of whether you agree to the privacy policy. Tap the [%s] on the right to review it again." + }, + { + "name": "setting_export", + "value": "Export token secret file (.atsf)" + }, + { + "name": "setting_export_pick_single", + "value": "Please choose exactly one file path." + }, + { + "name": "setting_export_success", + "value": "Export completed." + }, + { + "name": "setting_export_desc", + "value": "The token secret file contains all token secrets stored in the app. The file is not encrypted, so please keep it safe." + }, + { + "name": "setting_version_format", + "value": "Version: %s(%d)" + }, + { + "name": "setting_qq_group_format", + "value": "User QQ group: %s" + }, + { + "name": "privacy_title", + "value": "Privacy Policy" + }, + { + "name": "privacy_content", + "value": "更新日期:2025/04/03\n生效日期:2025/04/03\n导言\n千维验证器是一款由 郭维 (以下简称“我们”)提供的产品。 您在使用我们的服务时,我们不会收集和使用您的相关信息,我们希望通过本《隐私政策》向您说明。本《隐私政策》与您所使用的千维验证器服务息息相关,希望您仔细阅读,在需要时,按照本《隐私政策》的指引,作出您认为适当的选择。本《隐私政策》中涉及的相关技术词汇,我们尽量以简明扼要的表述,并提供进一步说明的链接,以便您的理解。\n\n您使用或继续使用我们的服务,即意味着同意本《隐私政策》。\n\n感谢您使用我们的服务!我们重视您的隐私,并承诺保护您的个人信息。在使用本服务时,我们希望向您明确声明,我们不会主动收集、存储或共享任何个人信息。请您在使用本服务前,仔细阅读本隐私政策。\n\n1. 我们不收集个人信息\n在您使用本服务的过程中,我们不会主动收集您的任何个人信息。无论您是匿名使用服务,还是以其他方式与我们进行互动,我们都不会要求或获取您的姓名、电子邮件地址、电话号码、住址、支付信息等个人身份信息。\n\n2. 不涉及第三方数据共享\n由于我们不收集任何个人信息,我们也不会将您的任何数据与第三方共享、出售或租赁。您的隐私安全对我们来说至关重要,我们不会向任何第三方提供与您身份相关的任何数据。\n\n3. 技术数据收集\n尽管我们不会收集您的个人信息,但我们可能会使用一些常见的技术手段来收集非个人信息,例如您的设备类型、浏览器类型、操作系统版本、访问日期和时间等。这些信息仅用于改善服务质量和用户体验,不会用于识别您个人身份。\n\n4. 数据存储\n由于我们不会收集个人信息,因此不会在服务器上存储任何与您身份相关的个人数据。任何与服务有关的日志数据都仅用于技术分析和系统维护,且不会用于任何身份识别。\n\n5. 安全性\n虽然我们不收集您的个人信息,但我们依然采取合理的安全措施来保护我们服务的数据安全。这些措施包括但不限于加密技术、服务器安全和网络防护,以确保我们的系统免受外部攻击或数据泄露。\n\n6. 儿童隐私\n我们的服务并不针对儿童,我们不会 knowingly 收集或请求未满18岁用户的个人信息。如果我们发现收集了未满18岁用户的个人信息,我们将尽快删除相关数据。\n\n7. 政策变更\n我们可能会根据法律、政策或服务改进的需要,适时更新或修改本隐私政策。如果本隐私政策发生重要变更,我们将在适当的地方进行通知,以确保您及时了解我们的隐私保护措施。\n\n8. 联系我们\n如果您对本隐私政策有任何疑问或需要更多信息,请随时联系我们。" + }, + { + "name": "privacy_disagree", + "value": "Disagree" + }, + { + "name": "privacy_agree", + "value": "Agree" + }, + { + "name": "confirm_detail_title", + "value": "Confirmation details" + }, + { + "name": "confirm_detail_headline", + "value": "Title" + }, + { + "name": "confirm_detail_summary", + "value": "Summary" + }, + { + "name": "confirm_detail_created_at", + "value": "Created at" + }, + { + "name": "confirm_detail_creator_id", + "value": "Creator ID" + }, + { + "name": "confirm_detail_type_id", + "value": "Type ID" + }, + { + "name": "confirm_detail_type_name", + "value": "Type name" + }, + { + "name": "confirm_detail_confirm_id", + "value": "Confirmation ID" + }, + { + "name": "confirm_disposable_number", + "value": "Disposable number" + }, + { + "name": "confirm_detail_icon_url", + "value": "Icon URL" + }, + { + "name": "confirm_list_title", + "value": "Steam confirmations" + }, + { + "name": "confirm_list_type_test", + "value": "Test" + }, + { + "name": "confirm_list_type_trade", + "value": "Trade" + }, + { + "name": "confirm_list_type_sell", + "value": "Sell" + }, + { + "name": "confirm_list_type_change_phone", + "value": "Change phone number" + }, + { + "name": "confirm_list_type_remove_phone", + "value": "Remove phone number" + }, + { + "name": "confirm_list_type_create_key", + "value": "Create access key" + }, + { + "name": "confirm_list_type_unknown", + "value": "Unknown type" + }, + { + "name": "confirm_list_query_failed_format", + "value": "Query failed: %s" + }, + { + "name": "confirm_list_query_success_format", + "value": "Query succeeded. Pending items: %d" + }, + { + "name": "confirm_list_allow_all", + "value": "Allow all?" + }, + { + "name": "confirm_list_deny_all", + "value": "Deny all?" + }, + { + "name": "confirm_list_allow_failed_format", + "value": "Allow request failed: %s" + }, + { + "name": "confirm_list_deny_failed_format", + "value": "Deny request failed: %s" + }, + { + "name": "confirm_list_loading", + "value": "Querying..." + }, + { + "name": "steam_intro_title", + "value": "How it works" + }, + { + "name": "steam_intro_steps", + "value": "1. Sign in to Steam\n2. Add an authenticator to Steam" + }, + { + "name": "steam_intro_note", + "value": "Note: You will need to enter your Steam password and verification code during the process. If requests time out or fail, enable Enhanced Network in Settings." + }, + { + "name": "steam_start_now", + "value": "Start now" + }, + { + "name": "steam_login_title", + "value": "Sign in to Steam" + }, + { + "name": "steam_login_prompt", + "value": "Enter your Steam account name and password" + }, + { + "name": "steam_account_name", + "value": "Account name" + }, + { + "name": "steam_password", + "value": "Password" + }, + { + "name": "steam_login_fetching_auth", + "value": "Steam login succeeded. Fetching authentication info..." + }, + { + "name": "steam_auth_fetch_failed_format", + "value": "Failed to fetch Steam authentication info: %s" + }, + { + "name": "steam_mfa_device_confirmation", + "value": "Steam has multi-factor authentication enabled. Confirm this sign-in on another device, then tap Complete sign-in." + }, + { + "name": "steam_mfa_email_confirmation", + "value": "Steam has multi-factor authentication enabled. A confirmation email has been sent. Tap the confirmation link in the email, then tap Complete sign-in." + }, + { + "name": "steam_mfa_device_code", + "value": "Steam has multi-factor authentication enabled. Get the token from your bound authenticator and enter it below." + }, + { + "name": "steam_mfa_email_code", + "value": "Steam has multi-factor authentication enabled. The token was sent by email. Enter it below." + }, + { + "name": "steam_complete_login", + "value": "Complete sign-in" + }, + { + "name": "steam_fetching_auth", + "value": "Fetching Steam authentication info..." + }, + { + "name": "steam_link_success_format", + "value": "Signed in successfully as %s. Tap Add to add Authenticator to Steam." + }, + { + "name": "steam_add_authenticator_failed_format", + "value": "Failed to add Authenticator: %s" + }, + { + "name": "steam_confirm_link_sms", + "value": "Steam is adding an authenticator. Enter the SMS verification code." + }, + { + "name": "steam_confirm_link_email", + "value": "Steam is adding an authenticator. The verification code was sent by email. Enter the code." + }, + { + "name": "steam_complete_add", + "value": "Complete add" + }, + { + "name": "steam_finalize_failed_format", + "value": "Failed to add authenticator: %s" + }, + { + "name": "steam_token_mismatch", + "value": "The generated token does not match Steam. Tap Complete add once more." + }, + { + "name": "steam_add_success", + "value": "Added successfully." + }, + { + "name": "steam_transfer_prompt", + "value": "Another authenticator is already added. Do you want to transfer it?" + }, + { + "name": "steam_transfer_note", + "value": "Important: Make sure the Steam account is already linked to a phone number, otherwise transfer will fail." + }, + { + "name": "steam_transfer_button", + "value": "Transfer" + }, + { + "name": "steam_transfer_failed_format", + "value": "Failed to transfer authenticator: %s" + }, + { + "name": "steam_transfer_sms", + "value": "Steam is transferring the authenticator. Enter the SMS verification code." + }, + { + "name": "steam_complete_transfer", + "value": "Complete transfer" + }, + { + "name": "steam_refresh_title", + "value": "Refresh Steam auth expiry" + }, + { + "name": "steam_refresh_prompt", + "value": "Refreshing requires signing in to Steam again. Enter your password." + }, + { + "name": "steam_refresh_confirmation_type_error_format", + "value": "Steam login succeeded, but the confirmation type is invalid: %d" + }, + { + "name": "steam_refresh_submitting", + "value": "Steam login succeeded. Submitting token..." + }, + { + "name": "steam_refresh_submit_failed_format", + "value": "Failed to submit Steam token: %s" + }, + { + "name": "steam_refresh_submit_success", + "value": "Steam token submitted successfully. Fetching authentication info..." + }, + { + "name": "steam_refresh_success", + "value": "Steam authentication info updated successfully." + }, + { + "name": "common_scan_failed_format", + "value": "Scan failed: %s" + }, + { + "name": "invalid_steam_qr_code", + "value": "Invalid Steam login QR code." + }, + { + "name": "icon_picker_auto", + "value": "Auto" + }, + { + "name": "icon_picker_recommended", + "value": "Recommended" + }, + { + "name": "icon_picker_title", + "value": "Choose icon" + }, + { + "name": "icon_picker_mode", + "value": "Display mode" } ] } \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/Aliyun.png b/entry/src/main/resources/rawfile/icons/aliyun.png similarity index 100% rename from entry/src/main/resources/rawfile/Aliyun.png rename to entry/src/main/resources/rawfile/icons/aliyun.png diff --git a/entry/src/main/resources/rawfile/Cloudflare.png b/entry/src/main/resources/rawfile/icons/cloudflare.png similarity index 100% rename from entry/src/main/resources/rawfile/Cloudflare.png rename to entry/src/main/resources/rawfile/icons/cloudflare.png diff --git a/entry/src/main/resources/rawfile/Epic Games.png b/entry/src/main/resources/rawfile/icons/epic_games.png similarity index 100% rename from entry/src/main/resources/rawfile/Epic Games.png rename to entry/src/main/resources/rawfile/icons/epic_games.png diff --git a/entry/src/main/resources/rawfile/Github.png b/entry/src/main/resources/rawfile/icons/github.png similarity index 100% rename from entry/src/main/resources/rawfile/Github.png rename to entry/src/main/resources/rawfile/icons/github.png diff --git a/entry/src/main/resources/rawfile/Gitlab.png b/entry/src/main/resources/rawfile/icons/gitlab.png similarity index 100% rename from entry/src/main/resources/rawfile/Gitlab.png rename to entry/src/main/resources/rawfile/icons/gitlab.png diff --git a/entry/src/main/resources/rawfile/Google.png b/entry/src/main/resources/rawfile/icons/google.png similarity index 100% rename from entry/src/main/resources/rawfile/Google.png rename to entry/src/main/resources/rawfile/icons/google.png diff --git a/entry/src/main/resources/rawfile/Huawei.png b/entry/src/main/resources/rawfile/icons/huawei.png similarity index 100% rename from entry/src/main/resources/rawfile/Huawei.png rename to entry/src/main/resources/rawfile/icons/huawei.png diff --git a/entry/src/main/resources/rawfile/Jetbrains.png b/entry/src/main/resources/rawfile/icons/jetbrains.png similarity index 100% rename from entry/src/main/resources/rawfile/Jetbrains.png rename to entry/src/main/resources/rawfile/icons/jetbrains.png diff --git a/entry/src/main/resources/rawfile/Microsoft.png b/entry/src/main/resources/rawfile/icons/microsoft.png similarity index 100% rename from entry/src/main/resources/rawfile/Microsoft.png rename to entry/src/main/resources/rawfile/icons/microsoft.png diff --git a/entry/src/main/resources/rawfile/Namecheap.png b/entry/src/main/resources/rawfile/icons/namecheap.png similarity index 100% rename from entry/src/main/resources/rawfile/Namecheap.png rename to entry/src/main/resources/rawfile/icons/namecheap.png diff --git a/entry/src/main/resources/rawfile/NeteaseMail.png b/entry/src/main/resources/rawfile/icons/neteasemail.png similarity index 100% rename from entry/src/main/resources/rawfile/NeteaseMail.png rename to entry/src/main/resources/rawfile/icons/neteasemail.png diff --git a/entry/src/main/resources/rawfile/Nintendo.png b/entry/src/main/resources/rawfile/icons/nintendo.png similarity index 100% rename from entry/src/main/resources/rawfile/Nintendo.png rename to entry/src/main/resources/rawfile/icons/nintendo.png diff --git a/entry/src/main/resources/rawfile/Nvidia.png b/entry/src/main/resources/rawfile/icons/nvidia.png similarity index 100% rename from entry/src/main/resources/rawfile/Nvidia.png rename to entry/src/main/resources/rawfile/icons/nvidia.png diff --git a/entry/src/main/resources/rawfile/Steam.png b/entry/src/main/resources/rawfile/icons/steam.png similarity index 100% rename from entry/src/main/resources/rawfile/Steam.png rename to entry/src/main/resources/rawfile/icons/steam.png diff --git a/entry/src/main/resources/rawfile/Twitter.png b/entry/src/main/resources/rawfile/icons/twitter.png similarity index 100% rename from entry/src/main/resources/rawfile/Twitter.png rename to entry/src/main/resources/rawfile/icons/twitter.png diff --git a/entry/src/main/resources/rawfile/Ubisoft.png b/entry/src/main/resources/rawfile/icons/ubisoft.png similarity index 100% rename from entry/src/main/resources/rawfile/Ubisoft.png rename to entry/src/main/resources/rawfile/icons/ubisoft.png diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json index aa353fd..e441aa6 100644 --- a/entry/src/main/resources/zh_CN/element/string.json +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -2,11 +2,11 @@ "string": [ { "name": "ACCESS_BIOMETRIC_reason", - "value": "用于在显示 2FA 密码前验证用户。" + "value": "用于在显示 2FA 密码前验证用户身份。" }, { "name": "CAMERA_reason", - "value": "用于扫描二维码。" + "value": "用于扫描二维码" }, { "name": "INTERNET_reason", @@ -18,11 +18,707 @@ }, { "name": "EntryAbility_desc", - "value": "description" + "value": "描述" }, { "name": "EntryAbility_label", - "value": "千维验证器" + "value": "KDAuth" + }, + { + "name": "common_cancel", + "value": "取消" + }, + { + "name": "common_confirm", + "value": "确认" + }, + { + "name": "common_add", + "value": "添加" + }, + { + "name": "common_login", + "value": "登录" + }, + { + "name": "common_yes", + "value": "是" + }, + { + "name": "common_safe_mode_unlock_first", + "value": "已启用安全模式,请先解锁。" + }, + { + "name": "common_enter_token_first", + "value": "请先输入令牌。" + }, + { + "name": "common_enter_code_first", + "value": "请先输入验证码。" + }, + { + "name": "base_unknown_file_format", + "value": "未知的文件格式。" + }, + { + "name": "base_invalid_scheme_format", + "value": "无效的协议:%s" + }, + { + "name": "base_recover_item_missing_format", + "value": "第 %d 个令牌密钥缺少 %s" + }, + { + "name": "base_missing_format", + "value": "缺少%s" + }, + { + "name": "add_title", + "value": "添加" + }, + { + "name": "add_totp_secret_title", + "value": "通过 TOTP 密钥添加" + }, + { + "name": "add_totp_secret_desc", + "value": "TOTP 密钥是由 A-Z 和 2-7 组成的字符串,例如:ABCDEFGHIJ234567" + }, + { + "name": "add_totp_uri_title", + "value": "通过 TOTP URI 添加" + }, + { + "name": "add_totp_uri_desc", + "value": "TOTP URI 是以 otpauth://totp/ 开头的链接,例如:otpauth://totp/Example:alice@example.com?secret=ABCDEFGHIJ234567\u0026issuer=Example" + }, + { + "name": "add_restore_title", + "value": "从令牌密钥文件(*.atsf)恢复" + }, + { + "name": "add_restore_desc", + "value": "令牌密钥文件(*.atsf)可从设置中导出。修改文件内容可能导致恢复失败。恢复不会覆盖已有令牌,如有重复请自行删除。" + }, + { + "name": "add_restore_pick_single", + "value": "请选择一个令牌密钥文件。" + }, + { + "name": "add_restore_success", + "value": "恢复完成。" + }, + { + "name": "add_restore_error_format", + "value": "恢复失败:%s" + }, + { + "name": "add_steam_title", + "value": "添加 Steam 令牌" + }, + { + "name": "add_steam_desc", + "value": "支持 5 位令牌、扫码登录和交易确认。" + }, + { + "name": "add_secret_title", + "value": "通过 TOTP 密钥添加" + }, + { + "name": "add_secret_provider", + "value": "提供方" + }, + { + "name": "add_secret_provider_placeholder", + "value": "请输入提供方名称" + }, + { + "name": "add_secret_account", + "value": "账号" + }, + { + "name": "add_secret_account_placeholder", + "value": "用户名或邮箱" + }, + { + "name": "add_secret_secret", + "value": "密钥" + }, + { + "name": "add_secret_secret_placeholder", + "value": "仅支持 A-Z 和 2-7" + }, + { + "name": "add_secret_digits", + "value": "令牌位数" + }, + { + "name": "add_secret_invalid_secret", + "value": "密钥只能由 A-Z 和 2-7 组成,且至少 2 个字符。" + }, + { + "name": "add_secret_error_format", + "value": "TOTP 错误:%s" + }, + { + "name": "add_uri_title", + "value": "通过 TOTP URI 添加" + }, + { + "name": "add_uri_heading", + "value": "输入 TOTP URI" + }, + { + "name": "add_uri_desc", + "value": "如果需要同时添加多个,请每行输入一个 URI。" + }, + { + "name": "add_uri_partial_error_format", + "value": "已添加%d个,第%d行的 TOTP URI 无效:%s" + }, + { + "name": "add_steam_secret_title", + "value": "添加 Steam 令牌" + }, + { + "name": "index_auth_title", + "value": "请进行身份认证" + }, + { + "name": "index_empty", + "value": "还没有添加过令牌密钥\n请点击右上角的加号或扫码按钮添加" + }, + { + "name": "index_add_success", + "value": "成功添加令牌。" + }, + { + "name": "index_no_steam_token", + "value": "没有 Steam 令牌,无法扫码登录 Steam,请先添加 Steam 令牌。" + }, + { + "name": "index_no_steam_auth", + "value": "Steam 令牌没有设置认证信息,无法扫码登录 Steam,请在令牌详情页设置。" + }, + { + "name": "index_go_to_steam_detail", + "value": "请在 Steam 令牌详情页扫码登录。" + }, + { + "name": "detail_steam_input_title", + "value": "输入 Steam 认证信息" + }, + { + "name": "detail_steam_input_desc", + "value": "认证通过后可以使用 Steam 扫码登录和交易确认功能。" + }, + { + "name": "detail_steam_input_error_format", + "value": "认证信息格式错误,请确保内容来自 maFile 文件:%s" + }, + { + "name": "detail_steam_invalid_error_format", + "value": "认证信息格式错误:%s,内容:%s" + }, + { + "name": "detail_steam_login_failed_format", + "value": "Steam 登录失败:%s" + }, + { + "name": "detail_steam_login_success", + "value": "Steam 登录成功。" + }, + { + "name": "detail_steam_login_start", + "value": "开始登录 Steam..." + }, + { + "name": "detail_steam_section_title", + "value": "----- Steam 认证信息 -----" + }, + { + "name": "detail_steam_section_desc", + "value": "如遇扫码登录和交易确认请求超时或失败,请在设置中开启增强访问。" + }, + { + "name": "detail_steam_account", + "value": "Steam 账号" + }, + { + "name": "detail_steam_id", + "value": "Steam ID" + }, + { + "name": "detail_device_id", + "value": "设备 ID" + }, + { + "name": "detail_expire_time", + "value": "过期时间" + }, + { + "name": "detail_reset", + "value": "重新设置" + }, + { + "name": "detail_update_expire", + "value": "更新过期时间" + }, + { + "name": "detail_setup_steam", + "value": "设置 Steam 认证信息" + }, + { + "name": "detail_alias", + "value": "别名" + }, + { + "name": "detail_account", + "value": "账号" + }, + { + "name": "detail_issuer", + "value": "发行商" + }, + { + "name": "detail_digits", + "value": "位数" + }, + { + "name": "setting_title", + "value": "设置" + }, + { + "name": "setting_auto_dark", + "value": "跟随系统深色模式" + }, + { + "name": "setting_auto_dark_desc", + "value": "启用后,App 会自动跟随系统深浅色模式切换。" + }, + { + "name": "setting_safe_mode", + "value": "启用安全模式" + }, + { + "name": "setting_safe_mode_desc", + "value": "启用后,打开 App 时需要通过生物识别或 PIN 验证后才能查看令牌。该设置会在下次启动时生效。" + }, + { + "name": "setting_hide_token", + "value": "隐藏令牌" + }, + { + "name": "setting_hide_token_desc", + "value": "启用后,首页令牌默认隐藏,点击后才显示。详情页不受影响。" + }, + { + "name": "icon_pack_title", + "value": "图标包" + }, + { + "name": "icon_pack_desc", + "value": "可使用内置提供方图标,也可导入兼容 Aegis 的 zip 图标包。" + }, + { + "name": "icon_pack_manage", + "value": "管理" + }, + { + "name": "icon_pack_hide", + "value": "收起" + }, + { + "name": "icon_pack_builtin", + "value": "内置图标" + }, + { + "name": "icon_pack_builtin_desc", + "value": "默认提供方图标库" + }, + { + "name": "icon_pack_import", + "value": "导入 zip" + }, + { + "name": "icon_pack_remove", + "value": "移除" + }, + { + "name": "icon_pack_import_success", + "value": "图标包导入成功" + }, + { + "name": "icon_pack_import_failed_format", + "value": "导入失败:%s" + }, + { + "name": "icon_pack_remove_success", + "value": "图标包已移除" + }, + { + "name": "icon_pack_remove_failed_format", + "value": "移除失败:%s" + }, + { + "name": "icon_pack_installed_count_format", + "value": "已安装 %d 个" + }, + { + "name": "icon_pack_invalid_name_format", + "value": "%s(无效)" + }, + { + "name": "icon_pack_invalid_desc", + "value": "pack.json 缺失或无效" + }, + { + "name": "icon_pack_meta_format", + "value": "%d 个图标 · v%s" + }, + { + "name": "setting_network", + "value": "增强访问" + }, + { + "name": "setting_network_enabled", + "value": "已启用开发者模式。" + }, + { + "name": "setting_network_disabled", + "value": "已关闭开发者模式。" + }, + { + "name": "setting_network_desc", + "value": "启用后,可增强通过令牌访问特定服务时的稳定性。" + }, + { + "name": "setting_privacy_policy", + "value": "隐私政策" + }, + { + "name": "setting_privacy_agreed", + "value": "已同意" + }, + { + "name": "setting_privacy_disagreed", + "value": "未同意" + }, + { + "name": "setting_privacy_desc_prefix", + "value": "无论是否同意隐私政策,均可使用所有功能。点击选项右侧的【%s】,可再次查看隐私政策内容。" + }, + { + "name": "setting_export", + "value": "导出令牌密钥文件(.atsf)" + }, + { + "name": "setting_export_pick_single", + "value": "请选择一个保存路径。" + }, + { + "name": "setting_export_success", + "value": "导出完成。" + }, + { + "name": "setting_export_desc", + "value": "令牌密钥文件包含 App 内所有令牌的密钥,文件内容未加密,请妥善保管。" + }, + { + "name": "setting_version_format", + "value": "版本:%s(%d)" + }, + { + "name": "setting_qq_group_format", + "value": "用户 QQ 群:%s" + }, + { + "name": "privacy_title", + "value": "隐私政策" + }, + { + "name": "privacy_content", + "value": "更新日期:2025/04/03\n生效日期:2025/04/03\n导言\n千维验证器是一款由 郭维 (以下简称“我们”)提供的产品。 您在使用我们的服务时,我们不会收集和使用您的相关信息,我们希望通过本《隐私政策》向您说明。本《隐私政策》与您所使用的千维验证器服务息息相关,希望您仔细阅读,在需要时,按照本《隐私政策》的指引,作出您认为适当的选择。本《隐私政策》中涉及的相关技术词汇,我们尽量以简明扼要的表述,并提供进一步说明的链接,以便您的理解。\n\n您使用或继续使用我们的服务,即意味着同意本《隐私政策》。\n\n感谢您使用我们的服务!我们重视您的隐私,并承诺保护您的个人信息。在使用本服务时,我们希望向您明确声明,我们不会主动收集、存储或共享任何个人信息。请您在使用本服务前,仔细阅读本隐私政策。\n\n1. 我们不收集个人信息\n在您使用本服务的过程中,我们不会主动收集您的任何个人信息。无论您是匿名使用服务,还是以其他方式与我们进行互动,我们都不会要求或获取您的姓名、电子邮件地址、电话号码、住址、支付信息等个人身份信息。\n\n2. 不涉及第三方数据共享\n由于我们不收集任何个人信息,我们也不会将您的任何数据与第三方共享、出售或租赁。您的隐私安全对我们来说至关重要,我们不会向任何第三方提供与您身份相关的任何数据。\n\n3. 技术数据收集\n尽管我们不会收集您的个人信息,但我们可能会使用一些常见的技术手段来收集非个人信息,例如您的设备类型、浏览器类型、操作系统版本、访问日期和时间等。这些信息仅用于改善服务质量和用户体验,不会用于识别您个人身份。\n\n4. 数据存储\n由于我们不会收集个人信息,因此不会在服务器上存储任何与您身份相关的个人数据。任何与服务有关的日志数据都仅用于技术分析和系统维护,且不会用于任何身份识别。\n\n5. 安全性\n虽然我们不收集您的个人信息,但我们依然采取合理的安全措施来保护我们服务的数据安全。这些措施包括但不限于加密技术、服务器安全和网络防护,以确保我们的系统免受外部攻击或数据泄露。\n\n6. 儿童隐私\n我们的服务并不针对儿童,我们不会 knowingly 收集或请求未满18岁用户的个人信息。如果我们发现收集了未满18岁用户的个人信息,我们将尽快删除相关数据。\n\n7. 政策变更\n我们可能会根据法律、政策或服务改进的需要,适时更新或修改本隐私政策。如果本隐私政策发生重要变更,我们将在适当的地方进行通知,以确保您及时了解我们的隐私保护措施。\n\n8. 联系我们\n如果您对本隐私政策有任何疑问或需要更多信息,请随时联系我们。" + }, + { + "name": "privacy_disagree", + "value": "不同意" + }, + { + "name": "privacy_agree", + "value": "同意" + }, + { + "name": "confirm_detail_title", + "value": "确认详情" + }, + { + "name": "confirm_detail_headline", + "value": "标题" + }, + { + "name": "confirm_detail_summary", + "value": "描述" + }, + { + "name": "confirm_detail_created_at", + "value": "创建时间" + }, + { + "name": "confirm_detail_creator_id", + "value": "创建者 ID" + }, + { + "name": "confirm_detail_type_id", + "value": "类型 ID" + }, + { + "name": "confirm_detail_type_name", + "value": "类型名称" + }, + { + "name": "confirm_detail_confirm_id", + "value": "确认 ID" + }, + { + "name": "confirm_disposable_number", + "value": "一次性编号" + }, + { + "name": "confirm_detail_icon_url", + "value": "图标 URL" + }, + { + "name": "confirm_list_title", + "value": "Steam 确认列表" + }, + { + "name": "confirm_list_type_test", + "value": "测试" + }, + { + "name": "confirm_list_type_trade", + "value": "交易" + }, + { + "name": "confirm_list_type_sell", + "value": "出售" + }, + { + "name": "confirm_list_type_change_phone", + "value": "更换手机号" + }, + { + "name": "confirm_list_type_remove_phone", + "value": "移除手机号" + }, + { + "name": "confirm_list_type_create_key", + "value": "创建访问密钥" + }, + { + "name": "confirm_list_type_unknown", + "value": "未知类型" + }, + { + "name": "confirm_list_query_failed_format", + "value": "查询失败:%s" + }, + { + "name": "confirm_list_query_success_format", + "value": "查询成功,待处理数量:%d" + }, + { + "name": "confirm_list_allow_all", + "value": "全部允许?" + }, + { + "name": "confirm_list_deny_all", + "value": "全部拒绝?" + }, + { + "name": "confirm_list_allow_failed_format", + "value": "确认请求失败:%s" + }, + { + "name": "confirm_list_deny_failed_format", + "value": "拒绝请求失败:%s" + }, + { + "name": "confirm_list_loading", + "value": "查询中..." + }, + { + "name": "steam_intro_title", + "value": "流程说明" + }, + { + "name": "steam_intro_steps", + "value": "1. 登录 Steam\n2. 向 Steam 中添加身份验证器" + }, + { + "name": "steam_intro_note", + "value": "注:期间需要输入 Steam 密码和验证码。如遇请求超时或请求失败等问题,请在设置中开启增强访问。" + }, + { + "name": "steam_start_now", + "value": "立即开始" + }, + { + "name": "steam_login_title", + "value": "登录 Steam" + }, + { + "name": "steam_login_prompt", + "value": "请输入 Steam 账户名称和密码" + }, + { + "name": "steam_account_name", + "value": "账户名称" + }, + { + "name": "steam_password", + "value": "密码" + }, + { + "name": "steam_login_fetching_auth", + "value": "Steam 登录成功,正在获取认证信息..." + }, + { + "name": "steam_auth_fetch_failed_format", + "value": "Steam 认证信息获取失败:%s" + }, + { + "name": "steam_mfa_device_confirmation", + "value": "Steam 启用了多重身份验证,请在其他设备上确认本次登录,确认完成后再点击【完成登录】。" + }, + { + "name": "steam_mfa_email_confirmation", + "value": "Steam 启用了多重身份验证,确认邮件已发送,请点击邮件内的确认链接后再点击【完成登录】。" + }, + { + "name": "steam_mfa_device_code", + "value": "Steam 启用了多重身份验证,请从已绑定的身份验证器获取令牌并输入。" + }, + { + "name": "steam_mfa_email_code", + "value": "Steam 启用了多重身份验证,令牌已通过邮件发送,请输入令牌。" + }, + { + "name": "steam_complete_login", + "value": "完成登录" + }, + { + "name": "steam_fetching_auth", + "value": "正在获取 Steam 认证信息..." + }, + { + "name": "steam_link_success_format", + "value": "%s登录成功,请点击【添加】按钮将 Authenticator 添加到 Steam。" + }, + { + "name": "steam_add_authenticator_failed_format", + "value": "添加 Authenticator 失败:%s" + }, + { + "name": "steam_confirm_link_sms", + "value": "正在向 Steam 添加身份验证器,请输入短信验证码。" + }, + { + "name": "steam_confirm_link_email", + "value": "正在向 Steam 添加身份验证器,验证码已通过邮件发送,请输入验证码。" + }, + { + "name": "steam_complete_add", + "value": "完成添加" + }, + { + "name": "steam_finalize_failed_format", + "value": "添加身份验证器失败:%s" + }, + { + "name": "steam_token_mismatch", + "value": "动态生成的令牌和 Steam 不一致,请再点击一次【完成添加】。" + }, + { + "name": "steam_add_success", + "value": "添加成功。" + }, + { + "name": "steam_transfer_prompt", + "value": "已经添加过其他身份验证器,是否迁移?" + }, + { + "name": "steam_transfer_note", + "value": "特别说明:请确保 Steam 账户已经绑定了手机号码,否则将无法进行迁移。" + }, + { + "name": "steam_transfer_button", + "value": "迁移" + }, + { + "name": "steam_transfer_failed_format", + "value": "迁移身份验证器失败:%s" + }, + { + "name": "steam_transfer_sms", + "value": "正在迁移 Steam 身份验证器,请输入短信验证码。" + }, + { + "name": "steam_complete_transfer", + "value": "完成迁移" + }, + { + "name": "steam_refresh_title", + "value": "更新 Steam 认证信息过期时间" + }, + { + "name": "steam_refresh_prompt", + "value": "更新需要重新登录 Steam,请输入密码" + }, + { + "name": "steam_refresh_confirmation_type_error_format", + "value": "Steam 登录成功,但确认类型错误:%d" + }, + { + "name": "steam_refresh_submitting", + "value": "Steam 登录成功,正在提交令牌..." + }, + { + "name": "steam_refresh_submit_failed_format", + "value": "Steam 令牌提交失败:%s" + }, + { + "name": "steam_refresh_submit_success", + "value": "Steam 令牌提交成功,正在获取认证信息..." + }, + { + "name": "steam_refresh_success", + "value": "更新 Steam 认证信息成功。" + }, + { + "name": "common_scan_failed_format", + "value": "扫码失败:%s" + }, + { + "name": "invalid_steam_qr_code", + "value": "无效的 Steam 登陆二维码" + }, + { + "name": "icon_picker_auto", + "value": "自动" + }, + { + "name": "icon_picker_recommended", + "value": "推荐" + }, + { + "name": "icon_picker_title", + "value": "选择图标" + }, + { + "name": "icon_picker_mode", + "value": "显示方式" } ] } \ No newline at end of file