Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
feat: support add custom icon
Browse files Browse the repository at this point in the history
  • Loading branch information
iamhyc committed Dec 7, 2024
1 parent 0b9dcf2 commit 24ae2c5
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 30 deletions.
2 changes: 2 additions & 0 deletions entry/src/main/ets/common/events.ets
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AuthLevelSupport } from "../crypto/authUtils";
import { IssuerName } from "../crypto/otpUtils";
import { ImageMimeType } from "./schema";
import { ValueType } from "./settings";

export const EVENT_CODE_REQUEST: string = 'code-request';
Expand All @@ -16,6 +17,7 @@ export interface UpdateRequestSchema {
keyAlias?: string, // used for self-update
issuerName?: IssuerName, // used for overwrite
icon?: string,
icon_mime?: ImageMimeType,
}
export const EVENT_UPDATE_ITEMS: string = 'update-items';
export interface BulkyUpdateRequestSchema {
Expand Down
10 changes: 8 additions & 2 deletions entry/src/main/ets/common/icons.ets
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fileIo as fs } from '@kit.CoreFileKit';
import { IconPackSchema, IconPackStore } from "./schema";
import { IconPackSchema, IconPackStore, ImageMimeType } from "./schema";
import { buffer } from '@kit.ArkTS';

interface IconFile {
Expand Down Expand Up @@ -157,13 +157,19 @@ export class IconManager {
return results;
}

static validateUri(uri: string): string {
static validateUri(uri: string, mime?: ImageMimeType): string {
const resDir = getContext().resourceDir;
const isAbsPath = uri.startsWith('/data/storage');

uri = uri.trim();
if (uri==='') {
return '';
} else if (mime) {
if (uri.startsWith('data:')) {
return uri;
} else {
return `data:${mime};base64,${uri}`;
}
} else if (isAbsPath && fs.accessSync(uri)) {
return 'file://'+uri;
} else if (!isAbsPath && fs.accessSync(resDir+'/'+uri)) {
Expand Down
2 changes: 1 addition & 1 deletion entry/src/main/ets/common/schema.ets
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface CodeResponseSchema {
next?: string,
}

export type ImageMimeType = 'image/svg+xml';
export type ImageMimeType = 'image/svg+xml'|'image/png'|'image/jpeg'|'image/webp';

export interface OTPItemInfo {
uuid?: string,
Expand Down
16 changes: 9 additions & 7 deletions entry/src/main/ets/entryability/EntryAbility.ets
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { CodeResponseSchema, OTPItemInfo, SecretSchema,
MasterKeyInfo,
IconPackStore,
IconPackSchema,
AesGcmMaterial} from '../common/schema'
AesGcmMaterial,
ImageMimeType} from '../common/schema'
import { arrayRearrange, b32decode, isValidBase32String, stringToUint8Array,
Uint8ArrayToString } from '../common/utils';
import { AIGIS_PREF_NAME, FAKE_OTP_CODE,
Expand Down Expand Up @@ -302,7 +303,7 @@ class PreferencesManager {
return failed;
}

async updatePreferenceItem(uri: string, keyAlias?: string, issuerName?: IssuerName, icon?: string) {
async updatePreferenceItem(uri: string, keyAlias?: string, issuerName?: IssuerName, icon?: string, icon_mime?: ImageMimeType) {
const args = parseURI(uri);
const otp = OTP.fromArguments(args);
const secret = args.get('secret');
Expand All @@ -315,7 +316,7 @@ class PreferencesManager {
if (collidedItem) {
keyAlias = collidedItem.keyAlias;
} else {
return await this.appendPreferenceItem(otp, secret!, icon);
return await this.appendPreferenceItem(otp, secret!, icon, icon_mime);
}
}
// find old items and associated secret
Expand All @@ -325,6 +326,7 @@ class PreferencesManager {
if (oldItem!==undefined) {
oldItem.schema = otp.schema;
oldItem.icon = icon ?? await this.iconManager.match( otp.schema.issuer );
oldItem.icon_mime = icon_mime
// update secret
if (secret && oldSecret) {
const result = await this.encryptSecret(secret);
Expand All @@ -338,11 +340,11 @@ class PreferencesManager {
}
// create new item
else {
return await this.appendPreferenceItem(otp, secret!, icon);
return await this.appendPreferenceItem(otp, secret!, icon, icon_mime);
}
}

private async appendPreferenceItem(otp: OTP, _secret: string, icon?: string) {
private async appendPreferenceItem(otp: OTP, _secret: string, icon?: string, icon_mime?: ImageMimeType) {
const keyAlias = util.generateRandomUUID();
const schema = otp.schema;

Expand All @@ -353,7 +355,7 @@ class PreferencesManager {
// find issuer icon
icon = icon ?? await this.iconManager.match( otp.schema.issuer );
const code: CodeResponseSchema = {timestamp:0, code:FAKE_OTP_CODE};
this._items.push({keyAlias, icon, code, schema});
this._items.push({keyAlias, icon, icon_mime, code, schema});
this._secrets.push({keyAlias, secret, encrypted});
this._instances.set(keyAlias, this.getOtpInstance(keyAlias));
}
Expand Down Expand Up @@ -669,7 +671,7 @@ export default class EntryAbility extends UIAbility {
this.context.eventHub.on(EVENT_UPDATE_ITEM, async (data: UpdateRequestSchema) => {
if (this.instPreferences) {
// update local preferences
await this.instPreferences.updatePreferenceItem(data.uri, data.keyAlias, data.issuerName, data.icon);
await this.instPreferences.updatePreferenceItem(data.uri, data.keyAlias, data.issuerName, data.icon, data.icon_mime);
// update persist preferences
this.instPreferences.persists(this.dataPreferences!);
// propagate updated items
Expand Down
18 changes: 12 additions & 6 deletions entry/src/main/ets/pages/Index.ets
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PasswordInputDialog, TextInputDialog } from '../components/dialog';
import { SettingsPage } from './SettingsPage';
import { acquireAtlAuth, ATL1AuthAvailable, ATL3AuthAvailable, validateAtlAuth,
validatePassword } from '../crypto/authUtils';
import { AtlAuthResult, CodeResponseSchema, MasterKeyInfo, OTPItemInfo } from '../common/schema';
import { AtlAuthResult, CodeResponseSchema, ImageMimeType, MasterKeyInfo, OTPItemInfo } from '../common/schema';
import {
BulkyUpdateRequestSchema,
CodeRequestSchema, EVENT_CODE_REQUEST,
Expand Down Expand Up @@ -42,15 +42,17 @@ class EditState {
public editKeyAlias: string = '';
public editSchema: OTPSchema | undefined = undefined;
public editIcon: string = '';
public editIconMime: ImageMimeType | undefined = undefined;
public editNewItem: boolean = false;

constructor() {}

show(editKeyAlias: string, editSchema: OTPSchema, editIcon: string, editNewItem: boolean = false) {
show(editKeyAlias: string, editSchema: OTPSchema, editIcon: string, editIconMime: ImageMimeType | undefined, editNewItem: boolean = false) {
this.editKeyAlias = editKeyAlias;
this.editSchema = editSchema;
this.editNewItem = editNewItem;
this.editIcon = editIcon;
this.editIconMime = editIconMime;
this.editPageShow = true;
}

Expand All @@ -59,6 +61,7 @@ class EditState {
this.editKeyAlias = '';
this.editSchema = undefined;
this.editIcon = '';
this.editIconMime = undefined;
this.editNewItem = false;
}
}
Expand Down Expand Up @@ -160,6 +163,7 @@ struct Index {
this.editState.editKeyAlias,
this.editState.editSchema!,
this.editState.editIcon,
this.editState.editIconMime,
this.editState.editNewItem,
),
{
Expand Down Expand Up @@ -515,6 +519,7 @@ struct OTPList {
OTPItem({
item,
icon: item.icon,
icon_mime: item.icon_mime,
code: item.code,
schema: item.schema,
favorite: item.favorite,
Expand Down Expand Up @@ -581,11 +586,12 @@ struct OTPItem {
@Link dragState: DragState<OTPItemInfo>;
@Prop item: OTPItemInfo;
@Prop icon: string;
@Prop icon_mime: ImageMimeType | undefined;
@Prop code: CodeResponseSchema;
@Prop keyAlias: string;
@Prop schema: OTPSchema;
@Prop favorite: boolean | undefined;
@State validatedIcon: string = IconManager.validateUri(this.icon);
@State validatedIcon: string = IconManager.validateUri(this.icon, this.icon_mime);
@Watch('onTimePassed') @State passed_time: number = 0;
@State showPreviewToken: boolean = false;
@State isVisible: Visibility = Visibility.Visible;
Expand Down Expand Up @@ -620,7 +626,7 @@ struct OTPItem {
},
onAction: () => {
this.selected = '';
this.editState.show(this.keyAlias, this.schema, this.icon);
this.editState.show(this.keyAlias, this.schema, this.icon, this.icon_mime);
this.listScroller!.closeAllSwipeActions();
},
},
Expand Down Expand Up @@ -799,7 +805,7 @@ struct OTPItem {
.backgroundColor($r('sys.color.brand'))
.onClick(() => {
this.selected = '';
this.editState.show(this.keyAlias, this.schema, this.icon);
this.editState.show(this.keyAlias, this.schema, this.icon, this.icon_mime);
this.listScroller?.closeAllSwipeActions();
})
}
Expand Down Expand Up @@ -1111,7 +1117,7 @@ struct QrScanButton {
getContext(this).eventHub.emit(EVENT_UPDATE_ITEM, {uri} as UpdateRequestSchema);
} else {
this.selected = '';
this.editState.show('', otp.secret_leaked_schema, '', true);
this.editState.show('', otp.secret_leaked_schema, '', undefined, true);
}
}

Expand Down
92 changes: 78 additions & 14 deletions entry/src/main/ets/pages/ItemEditPage.ets
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { promptAction } from "@kit.ArkUI";
import { fileIo as fs } from '@kit.CoreFileKit';
import { photoAccessHelper } from "@kit.MediaLibraryKit";

import { EVENT_REMOVE_ITEM, EVENT_UPDATE_ITEM, RemovalRequestSchema, UpdateRequestSchema } from "../common/events";
import { IconManager } from "../common/icons";
import { IconPackStore } from "../common/schema";
import { IconPackStore, ImageMimeType } from "../common/schema";
import { IssuerIcon } from "../components/icons";
import { LabeledSelect, LabeledTextInput, LabeledUriHolder } from "../components/labeled";
import { QrCodePage } from "../components/pages";
import { IssuerName, issuerNameToString, OTPSchema, TimedOTPSchema } from "../crypto/otpUtils";
import { util } from "@kit.ArkTS";
import { Uint8ArrayToString } from "../common/utils";

const COLUMN_ITEM_GAP: number = 8;
const FAKE_SECRET: string = '0'.repeat(16);
Expand All @@ -19,11 +24,13 @@ export function ItemEditBuilder(
keyAlias: string,
schema: OTPSchema,
icon: string,
icon_mime: ImageMimeType | undefined,
editNewItem: boolean,
) {
Column() {
ItemEditComponent({
context, exIssuerNames, keyAlias, schema, icon, editNewItem,
context, exIssuerNames, keyAlias, schema, editNewItem,
icon, icon_mime,
issuer: schema.issuer,
name: schema.name,
typeIndex: typeEntries.indexOf(schema.type.toUpperCase()),
Expand Down Expand Up @@ -52,6 +59,7 @@ struct ItemEditComponent {
@Watch('onValueChanged') @Require @Prop hashIndex: number;
@Watch('onValueChanged') @Require @Prop digits: string;
@Watch('onValueChanged') @Require @Prop icon: string;
@Prop icon_mime: ImageMimeType | undefined;
@Watch('onValueChanged') @Prop note: string;
@Watch('onValueChanged') @Prop period: string;
@Watch('onValueChanged') @Prop counter: string;
Expand All @@ -66,13 +74,14 @@ struct ItemEditComponent {
@State IconCandidates: Map<ResourceStr,string> = this.iconManager.matchAllSync(this.issuer);
@State IconCandidatesEntries: ResourceStr[] = Array.from(this.IconCandidates.keys());
@State IconCandidatesValues: string[] = Array.from(this.IconCandidates.values());
@State validatedIcon: string = IconManager.validateUri(this.editNewItem? this.IconCandidatesValues[0] : this.icon);
@State validatedIcon: string = IconManager.validateUri(this.editNewItem? this.IconCandidatesValues[0] : this.icon, this.icon_mime);
@Watch('onValueChanged') @State iconIndex: number = this.IconCandidatesValues.findIndex(x => x===this.icon);

private onValueChanged(propName: string) {
this.saveBtnEnabled = true;
if (propName==='iconIndex' && this.iconIndex >= 0) {
this.icon = this.IconCandidatesValues[ this.iconIndex ];
this.icon_mime = undefined;
this.validatedIcon = IconManager.validateUri(this.icon);
}
this.shareLink = this.fieldsToUri();
Expand Down Expand Up @@ -141,17 +150,18 @@ struct ItemEditComponent {

const uri = this.fieldsToUri();
const icon = this.icon;
const icon_mime = this.icon_mime;
const issuerNameCollided = this.exIssuerNames.find(x => x.issuer===this.issuer && x.name===this.name);
//
if (issuerNameCollided) {
const allow_overwrite = this.editNewItem; //overwrite when adding new ones
const issuerName = issuerNameCollided;
this.requestOverwriteConfirm(issuerNameToString(issuerName), allow_overwrite, () => {
getContext(this).eventHub.emit(EVENT_UPDATE_ITEM, {uri, issuerName, icon} as UpdateRequestSchema);
getContext(this).eventHub.emit(EVENT_UPDATE_ITEM, {uri, issuerName, icon, icon_mime} as UpdateRequestSchema);
});
} else {
const keyAlias = this.editNewItem? undefined : this.keyAlias;
getContext(this).eventHub.emit(EVENT_UPDATE_ITEM, {uri, keyAlias, icon} as UpdateRequestSchema);
getContext(this).eventHub.emit(EVENT_UPDATE_ITEM, {uri, keyAlias, icon, icon_mime} as UpdateRequestSchema);
}
}

Expand All @@ -168,6 +178,55 @@ struct ItemEditComponent {
return uri;
}

private async uploadIcon() {
const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.isPhotoTakingSupported = false;
photoSelectOptions.isPreviewForSingleSelectionSupported = true;
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
// open selected image file
const photoSelectResult = await photoViewPicker.select(photoSelectOptions);
const imageUri = photoSelectResult.photoUris[0];
const imageFile = await fs.open(imageUri);
// check file type
let icon_mime: ImageMimeType;
const lastDotIndex = imageUri.lastIndexOf('.');
const suffix = lastDotIndex !== -1 ? imageUri.substring(lastDotIndex) : '';
switch (suffix) {
// case '.svg': // FIXME: not supported in base64
// icon_mime = 'image/svg+xml';
// break;
case '.jpg':
case '.jpeg':
icon_mime = 'image/jpeg';
break;
case '.png':
icon_mime = 'image/png';
break;
case '.webp':
icon_mime = 'image/webp';
break;
default:
promptAction.showToast({message: $r('app.string.edit_icon_file_not_supported'), duration: 500});
return;
}
// check file size
const stat = await fs.stat(imageFile.fd);
if (stat.size >= 5*1024) { //5KB
promptAction.showToast({message:$r('app.string.edit_icon_file_too_large'), duration:500});
return;
}
//
const b64 = new util.Base64Helper();
const arrayBuffer = new ArrayBuffer(5*1024); //5KB
const readLen = await fs.read(imageFile.fd, arrayBuffer);
const content = Uint8ArrayToString(await b64.encode( new Uint8Array(arrayBuffer, 0, readLen) ));
this.icon_mime = icon_mime;
this.icon = content;
this.validatedIcon = IconManager.validateUri(content, this.icon_mime);
}

@Builder pageJump(name: string, params: string[]) {
NavDestination() {
if (name==='qrcode') {
Expand All @@ -193,24 +252,29 @@ struct ItemEditComponent {
.borderRadius(32)
.margin({bottom:COLUMN_ITEM_GAP})
} else {
Stack({alignContent: Alignment.BottomEnd}) {
Stack({alignContent: Alignment.Bottom}) {
IssuerIcon({
issuer: this.issuer,
iconSize: '64vp',
iconRadius: '32vp',
fontSize: 36,
})
Button() {
SymbolGlyph($r('sys.symbol.plus_square_on_square_fill'))
Button({type:ButtonType.Normal}) {
SymbolGlyph($r('sys.symbol.chevron_up_2'))
.fontWeight(FontWeight.Bolder)
.fontColor([$r('sys.color.font_primary')])
.fontSize(16)
.fontSize(12)
}
.offset({right:-16})
.backgroundColor($r('sys.color.ohos_id_color_component_activated_transparent'))
.width(32).height(32)
.backgroundColor($r('sys.color.ohos_id_color_component_normal_transparent'))
.width(64).height(16)
.onClick(async () => {
await this.uploadIcon();
})
}
.width(64).height(64)
.margin({bottom:COLUMN_ITEM_GAP})
.width(64).height(64)
.borderRadius(32)
.clip(true)
.margin({bottom:COLUMN_ITEM_GAP})
}
Divider().strokeWidth('1px').width('90%')
// information fields
Expand Down
Loading

0 comments on commit 24ae2c5

Please sign in to comment.