diff --git a/huawei/atomicservice/MFA/entry/src/main/ets/api/Totp.ets b/huawei/atomicservice/MFA/entry/src/main/ets/api/Totp.ets index 577ef16a..fd32e41c 100644 --- a/huawei/atomicservice/MFA/entry/src/main/ets/api/Totp.ets +++ b/huawei/atomicservice/MFA/entry/src/main/ets/api/Totp.ets @@ -30,6 +30,18 @@ export interface DeleteRequest { id: string; } +export interface SortRequest { + items: Array; +} + +export interface SortRequestItem { + id: string; + sort: number; +} + + +export interface SortResponse {} + export class Totp { static async all(): Promise> { const response: AxiosResponse>> = @@ -113,4 +125,18 @@ export class Totp { return Promise.reject(new HttpError(response.data.message)); } -} \ No newline at end of file + + static async sort(items: Array): Promise { + const response: AxiosResponse> = + await http.post>, SortRequest>('api/v1/totp/sort', + { + items, + }); + + if (0 === response.data?.code) { + return; + } + + return Promise.reject(new HttpError(response.data.message)); + } +} diff --git a/huawei/atomicservice/MFA/entry/src/main/ets/models/Runtime.ets b/huawei/atomicservice/MFA/entry/src/main/ets/models/Runtime.ets index 3d127c16..37d047cd 100644 --- a/huawei/atomicservice/MFA/entry/src/main/ets/models/Runtime.ets +++ b/huawei/atomicservice/MFA/entry/src/main/ets/models/Runtime.ets @@ -169,15 +169,15 @@ export class ItemsRuntime { } catch (error) { } - for (const item of items) { + for (const item of Array.from(items).sort((a: IItem, b: IItem): number => b.sort - a.sort)) { this.add(item); } } @Trace - private _ids: Set = new Set(); + private _ids: string[] = []; - get ids(): Set { + get ids(): string[] { return this._ids; } @@ -192,7 +192,12 @@ export class ItemsRuntime { delete(id: string) { this.stop(id); - this._ids.delete(id); + const index = this._ids.indexOf(id); + + if (index >= 0) { + this._ids.splice(index, 1); + this._ids = [...this._ids]; + } try { this._runtimes.remove(id); @@ -212,11 +217,11 @@ export class ItemsRuntime { } isEmpty(): boolean { - return this._ids.size > 0; + return this._ids.length === 0; } clear() { - this._ids.forEach((id: string): void => this.delete(id)); + [...this._ids].forEach((id: string): void => this.delete(id)); } add(item: IItem): ItemRuntime { @@ -226,10 +231,35 @@ export class ItemsRuntime { try { this._runtimes.set(item.id, runtime); - this._ids.add(item.id); + + const index = this._ids.indexOf(item.id); + + if (index >= 0) { + this._ids.splice(index, 1); + } + + this._ids.push(item.id); + this._ids = [...this._ids]; } catch (error) { } return runtime; } -} \ No newline at end of file + + move(fromIndex: number, toIndex: number) { + if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= this._ids.length || + toIndex >= this._ids.length) { + return; + } + + const ids = [...this._ids]; + const movedId = ids.splice(fromIndex, 1)[0]; + + if ('undefined' === typeof movedId) { + return; + } + + ids.splice(toIndex, 0, movedId); + this._ids = ids; + } +} diff --git a/huawei/atomicservice/MFA/entry/src/main/ets/pages/index/Index.ets b/huawei/atomicservice/MFA/entry/src/main/ets/pages/index/Index.ets index fa605151..ba7a7d1c 100644 --- a/huawei/atomicservice/MFA/entry/src/main/ets/pages/index/Index.ets +++ b/huawei/atomicservice/MFA/entry/src/main/ets/pages/index/Index.ets @@ -5,7 +5,8 @@ import { AppStorageV2, LoadingDialogV2, PersistenceV2, - SymbolGlyphModifier + SymbolGlyphModifier, + promptAction } from '@kit.ArkUI'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { Authorization } from '../../models/Auth'; @@ -16,6 +17,11 @@ import { scanBarcode } from '@kit.ScanKit'; import { User } from '../../models/User'; import { User as UserApi } from '../../api/User'; +@ObservedV2 +export class IndexGuideState { + @Trace isSkipped: boolean = false; +} + @Builder export function builder() { Index() @@ -23,12 +29,18 @@ export function builder() { @ComponentV2 export struct Index { + @Local guideState: IndexGuideState = PersistenceV2.globalConnect({ type: IndexGuideState, defaultCreator: () => new IndexGuideState() })!; @Local authorization: Authorization = PersistenceV2.globalConnect({ type: Authorization, defaultCreator: () => new Authorization() })!; @Local private isRefreshing: boolean = false; @Local + private isSortMode: boolean = false; + @Local + private draggedIndex: number = -1; + private snapshotIds: string[] = []; + @Local private itemsRuntime: ItemsRuntime = new ItemsRuntime(); private user: User = PersistenceV2.globalConnect({ type: User, defaultCreator: () => new User() })!; @@ -50,8 +62,8 @@ export struct Index { NavDestination() { Stack({ alignContent: Alignment.BottomEnd }) { Refresh({ refreshing: this.isRefreshing!! }) { - if (this.itemsRuntime.ids.size <= 0) { - this.empty() + if (this.itemsRuntime.ids.length <= 0) { + this.guide() } else { this.main() } @@ -64,54 +76,56 @@ export struct Index { .refreshOffset(64) .pullToRefresh(true) - Button({ type: ButtonType.Circle }) { - Row() { - Text() { - SymbolSpan($r('sys.symbol.plus')) - .fontSize(35) - .fontColor([$r('sys.color.font_on_primary')]) + if (!this.isSortMode) { + Button({ type: ButtonType.Circle }) { + Row() { + Text() { + SymbolSpan($r('sys.symbol.plus')) + .fontSize(35) + .fontColor([$r('sys.color.font_on_primary')]) + } } } - } - .backgroundColor($r('app.color.brand')) - .fontColor($r('sys.color.font_on_primary')) - .padding({ - top: $r('app.integer.page_padding'), - bottom: $r('app.integer.page_padding'), - left: $r('app.integer.page_padding'), - right: $r('app.integer.page_padding') - }) - .margin({ bottom: 50, right: 50 }) - .bindMenu([ - { - value: '扫码添加', - symbolIcon: new SymbolGlyphModifier($r('sys.symbol.line_viewfinder')), - action: () => { - scanBarcode.startScanForResult(this.getUIContext().getHostContext()).then(async (result) => { - let uri = result.originalValue; - - hilog.info(LogDomain.PAGES, AppPagesName.INDEX, '扫码结果: %{public}s', uri); - - try { - const data = - await Dialog.execute(() => this.loadingDialog($r('app.string.handling')), - () => Totp.create(uri)); - - this.itemsRuntime.add(data); - } catch (e) { - await Dialog.open(() => this.addFailedDialog(e.message)); - } - }); - }, - }, - { - value: '输入添加', - symbolIcon: new SymbolGlyphModifier($r('sys.symbol.doc_plaintext_and_pencil')), - action: () => { - this.pages.pushPathByName(AppPagesName.INDEX_CREATE, null); + .backgroundColor($r('app.color.brand')) + .fontColor($r('sys.color.font_on_primary')) + .padding({ + top: $r('app.integer.page_padding'), + bottom: $r('app.integer.page_padding'), + left: $r('app.integer.page_padding'), + right: $r('app.integer.page_padding') + }) + .margin({ bottom: 50, right: 50 }) + .bindMenu([ + { + value: '扫码添加', + symbolIcon: new SymbolGlyphModifier($r('sys.symbol.line_viewfinder')), + action: () => { + scanBarcode.startScanForResult(this.getUIContext().getHostContext()).then(async (result) => { + let uri = result.originalValue; + + hilog.info(LogDomain.PAGES, AppPagesName.INDEX, '扫码结果: %{public}s', uri); + + try { + const data = + await Dialog.execute(() => this.loadingDialog($r('app.string.handling')), + () => Totp.create(uri)); + + this.itemsRuntime.add(data); + } catch (e) { + await Dialog.open(() => this.addFailedDialog(e.message)); + } + }); + }, }, - } - ]) + { + value: '输入添加', + symbolIcon: new SymbolGlyphModifier($r('sys.symbol.doc_plaintext_and_pencil')), + action: () => { + this.pages.pushPathByName(AppPagesName.INDEX_CREATE, null); + }, + } + ]) + } } } .title(this.title()) @@ -133,49 +147,229 @@ export struct Index { @Builder title() { Row() { - Image(this.user.config.avatar ?? $r('app.media.default_avatar')) - .width(30) - .height(30) - .autoResize(true) - .interpolation(ImageInterpolation.Medium) - .margin({ right: 10 }) - .borderRadius(10) + if (this.isSortMode) { + Text("拖拽排序中") + .fontWeight(FontWeight.Bold) + .fontSize(20) + .layoutWeight(1) + + Button("完成") + .type(ButtonType.Capsule) + .backgroundColor($r('app.color.brand')) + .fontColor($r('sys.color.font_on_primary')) + .onClick(async () => { + let reqItems: Array<{ id: string, sort: number }> = []; + let len = this.itemsRuntime.ids.length; + this.itemsRuntime.ids.forEach((id, idx) => { + reqItems.push({ id: id, sort: len - 1 - idx }); + }); + + try { + const dialogId = await Dialog.open(() => this.loadingDialog()); + await Totp.sort(reqItems); + Dialog.close(dialogId); + this.isSortMode = false; + } catch (e) { + this.itemsRuntime.ids.splice(0, this.itemsRuntime.ids.length, ...this.snapshotIds); + promptAction.showToast({ message: '排序失败: ' + e.message }); + } + }) + } else { + Image(this.user.config.avatar ?? $r('app.media.default_avatar')) + .width(30) + .height(30) + .autoResize(true) + .interpolation(ImageInterpolation.Medium) + .margin({ right: 10 }) + .borderRadius(10) + .onClick(() => { + this.pages.pushPathByName(AppPagesName.USER_DETAIL, null); + }) + + Text($r("app.string.app_name_short")).fontWeight(FontWeight.Bold).fontSize(30) + + Blank() + + Button({ type: ButtonType.Circle }) { + SymbolGlyph($r('sys.symbol.arrow_up_arrow_down')) + .fontSize(24) + .fontColor([$r('sys.color.font_primary')]) + } + .backgroundColor(Color.Transparent) .onClick(() => { - this.pages.pushPathByName(AppPagesName.USER_DETAIL, null); + this.isSortMode = true; + this.snapshotIds = [...this.itemsRuntime.ids]; }) - - Text($r("app.string.app_name_short")).fontWeight(FontWeight.Bold).fontSize(30) + } } .width('100%') .height('100%') - .margin({ left: 10 }) + .margin({ left: 10, right: 10 }) .alignItems(VerticalAlign.Center) } @Builder - empty() { - Column() { - Text() { - SymbolSpan($r('sys.symbol.exclamationmark_circle')) + guide() { + if (this.guideState.isSkipped) { + Column() { + Text() { + SymbolSpan($r('sys.symbol.exclamationmark_circle')) + } + .fontSize(50) + + Text($r('app.string.index_index_page_data_empty')) + .margin({ top: 10 }) } - .fontSize(50) + .margin({ top: $r('app.integer.page_margin') }) + .width('100%') + .height('100%') + } else { + Stack({ alignContent: Alignment.TopEnd }) { + Stepper() { + StepperItem() { + Column() { + Text() { + SymbolSpan($r('sys.symbol.shield')) + } + .fontSize(100) + .fontColor([$r('app.color.brand')]) + + Text("MFA 验证器保护账号安全") + .margin({ top: 30 }) + .fontSize(20) + .fontWeight(FontWeight.Bold) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } + .nextLabel('下一步') + .prevLabel('上一步') + + StepperItem() { + Column() { + Text() { + SymbolSpan($r('sys.symbol.qrcode')) + } + .fontSize(100) + .fontColor([$r('app.color.brand')]) + + Button("扫码添加") + .margin({ top: 30 }) + .onClick(() => { + scanBarcode.startScanForResult(this.getUIContext().getHostContext()).then(async (result) => { + let uri = result.originalValue; + + hilog.info(LogDomain.PAGES, AppPagesName.INDEX, '扫码结果: %{public}s', uri); + + try { + const data = + await Dialog.execute(() => this.loadingDialog($r('app.string.handling')), + () => Totp.create(uri)); + + this.itemsRuntime.add(data); + } catch (e) { + await Dialog.open(() => this.addFailedDialog(e.message)); + } + }); + }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } + .nextLabel('下一步') + .prevLabel('上一步') + + StepperItem() { + Column() { + Text() { + SymbolSpan($r('sys.symbol.doc_plaintext_and_pencil')) + } + .fontSize(100) + .fontColor([$r('app.color.brand')]) + + Button("手动输入") + .margin({ top: 30 }) + .onClick(() => { + this.pages.pushPathByName(AppPagesName.INDEX_CREATE, null); + }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } + .nextLabel('下一步') + .prevLabel('上一步') + + StepperItem() { + Column() { + Text() { + SymbolSpan($r('sys.symbol.checkmark')) + } + .fontSize(100) + .fontColor([$r('app.color.brand')]) + + Text("验证码每 30 秒刷新") + .margin({ top: 30 }) + .fontSize(20) + .fontWeight(FontWeight.Bold) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + } + .nextLabel('开始使用') + .prevLabel('上一步') + } + .onFinish(() => { + this.guideState.isSkipped = true; + }) + .onSkip(() => { + this.guideState.isSkipped = true; + }) + + Button("跳过") + .type(ButtonType.Normal) + .backgroundColor(Color.Transparent) + .fontColor($r('sys.color.font_secondary')) + .margin({ top: 20, right: 20 }) + .onClick(() => { + this.guideState.isSkipped = true; + }) + } + .width('100%') + .height('100%') + } + } - Text($r('app.string.index_index_page_data_empty')) - .margin({ top: 10 }) + @Builder + dragPreview(id: string) { + Column() { + Text(this.itemsRuntime.get(id).issuer) + .fontSize(30) + .fontColor($r('sys.color.font_secondary')) } - .margin({ top: $r('app.integer.page_margin') }) - .width('100%') - .height('100%') + .padding(10) + .borderRadius(10) + .backgroundColor($r('sys.color.background_primary')) } @Builder main() { List({ scroller: this.scroller }) { - Repeat(Array.from(this.itemsRuntime.ids.values())) + Repeat(this.itemsRuntime.ids) .each((ri: RepeatItem) => { ListItem() { Column() { Row() { + if (this.isSortMode) { + SymbolGlyph($r('sys.symbol.line_3_horizontal')) + .fontSize(24) + .fontColor([$r('sys.color.font_tertiary')]) + .margin({ right: 10 }) + } + Column() { Column() { Text(this.itemsRuntime.get(ri.item.valueOf()).issuer) @@ -196,13 +390,13 @@ export struct Index { } .margin({ top: 5 }) } - .width('40%') + .layoutWeight(4) Column() { Text(this.itemsRuntime.get(ri.item.valueOf()).code) .fontSize(50) } - .width('60%') + .layoutWeight(6) } .height(98) .padding({ left: $r('app.integer.page_padding_smaller'), right: $r('app.integer.page_padding_smaller') }) @@ -219,7 +413,7 @@ export struct Index { .borderRadius(10) .height(100) .backgroundColor($r('sys.color.background_primary')) - .swipeAction({ + .swipeAction(this.isSortMode ? undefined : { end: { builder: () => { this.swipeAction(ri.item.valueOf()) @@ -227,6 +421,23 @@ export struct Index { }, edgeEffect: SwipeEdgeEffect.None, }) + .onDragStart((event: DragEvent, extraParams: string) => { + if (!this.isSortMode) { + return; + } + this.draggedIndex = ri.index; + return this.dragPreview(ri.item.valueOf()); + }) + .onDrop((event: DragEvent, extraParams: string) => { + if (!this.isSortMode) { + return; + } + let targetIndex = ri.index; + if (this.draggedIndex !== -1 && this.draggedIndex !== targetIndex) { + this.itemsRuntime.move(this.draggedIndex, targetIndex); + this.draggedIndex = -1; + } + }) .onAttach(() => { this.itemsRuntime.start(ri.item.valueOf()); }) @@ -234,11 +445,14 @@ export struct Index { this.itemsRuntime.stop(ri.item.valueOf()); }) .onClick(() => { + if (this.isSortMode) { + return; + } this.pages.pushPathByName(AppPagesName.INDEX_DETAIL, new ItemRuntime(this.itemsRuntime.get(ri.item.valueOf()))) }) }) - .virtualScroll({ totalCount: this.itemsRuntime.ids.size }) + .virtualScroll({ totalCount: this.itemsRuntime.ids.length }) } .width('100%') .height('100%') @@ -368,4 +582,4 @@ export struct Index { this.itemsRuntime = new ItemsRuntime(items.values()); } -} \ No newline at end of file +} diff --git a/huawei/atomicservice/MFA/entry/src/main/ets/types/Item.ets b/huawei/atomicservice/MFA/entry/src/main/ets/types/Item.ets index 892318de..42394bf6 100644 --- a/huawei/atomicservice/MFA/entry/src/main/ets/types/Item.ets +++ b/huawei/atomicservice/MFA/entry/src/main/ets/types/Item.ets @@ -3,6 +3,7 @@ export interface IItem { issuer: string; username: string; code: string; + sort: number; config: IItemConfig; }