|
|
@@ -3,7 +3,7 @@ import { WebDavAccount } from '../viewmodel/WebDavAccount';
|
|
|
import { Song } from '../viewmodel/Song';
|
|
|
import { RemoteDriveManagerStates } from '../common/enums/RemoteDriveManagerStates';
|
|
|
import Logger from '../common/util/Logger';
|
|
|
-import { promptAction, SymbolGlyphModifier,router, window } from '@kit.ArkUI';
|
|
|
+import { promptAction, SymbolGlyphModifier, router, window } from '@kit.ArkUI';
|
|
|
import { CommonConstants } from '../common/constants/CommonConstants';
|
|
|
import { VideoItem } from '../viewmodel/VideoItem';
|
|
|
import { GlobalContext } from '../common/util/GlobalContext';
|
|
|
@@ -102,6 +102,10 @@ export struct WebDavMainPage {
|
|
|
@State isLongNameRoLL: boolean = true//长歌名滚动
|
|
|
@State sortType: number = 0 //默认排序方式
|
|
|
@State listRefreshKey: number = 0 // 列表刷新标识
|
|
|
+ @State isMultiSelect: boolean = false
|
|
|
+ @State selectedSongs: VideoItem[] = []
|
|
|
+ @State isAllSelected: boolean = false
|
|
|
+ @State isDeletingSelection: boolean = false
|
|
|
private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
|
|
|
|
|
|
async onSwitchAccount(){
|
|
|
@@ -109,7 +113,8 @@ export struct WebDavMainPage {
|
|
|
this.songs = [];
|
|
|
this.visibleFoldersState = [];
|
|
|
this.updateListData(this.songs)
|
|
|
-
|
|
|
+ this.exitMultiSelect();
|
|
|
+
|
|
|
// 注意:不再需要清空全局上下文,因为LocalMusic已改用实例变量缓存
|
|
|
// 当新账户加载时,新的认证信息会自动覆盖旧的
|
|
|
Logger.info(TAG, 'heanup 切换账户,准备加载新账户文件');
|
|
|
@@ -178,6 +183,131 @@ export struct WebDavMainPage {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private isSongSelected(song: VideoItem): boolean {
|
|
|
+ return this.selectedSongs.some(item => item.filePath === song.filePath);
|
|
|
+ }
|
|
|
+
|
|
|
+ private enterMultiSelect(song: VideoItem): void {
|
|
|
+ if (!this.isMultiSelect) {
|
|
|
+ this.isMultiSelect = true;
|
|
|
+ this.selectedSongs = [song];
|
|
|
+ this.isAllSelected = this.selectedSongs.length === this.songs.length && this.songs.length > 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.isSongSelected(song)) {
|
|
|
+ this.toggleSongSelection(song);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private toggleSongSelection(song: VideoItem): void {
|
|
|
+ if (!this.isMultiSelect) {
|
|
|
+ this.isMultiSelect = true;
|
|
|
+ }
|
|
|
+ const exists = this.isSongSelected(song);
|
|
|
+ if (exists) {
|
|
|
+ const next = this.selectedSongs.filter(item => item.filePath !== song.filePath);
|
|
|
+ this.selectedSongs = next;
|
|
|
+ this.isAllSelected = next.length > 0 && next.length === this.songs.length;
|
|
|
+ if (next.length === 0) {
|
|
|
+ this.exitMultiSelect();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const next = [...this.selectedSongs, song];
|
|
|
+ this.selectedSongs = next;
|
|
|
+ this.isAllSelected = next.length === this.songs.length && this.songs.length > 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private handleCheckboxSelection(song: VideoItem, checked: boolean): void {
|
|
|
+ if (!this.isMultiSelect) {
|
|
|
+ this.isMultiSelect = true;
|
|
|
+ }
|
|
|
+ const exists = this.isSongSelected(song);
|
|
|
+ if (checked && !exists) {
|
|
|
+ this.toggleSongSelection(song);
|
|
|
+ } else if (!checked && exists) {
|
|
|
+ this.toggleSongSelection(song);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private toggleSelectAll(): void {
|
|
|
+ if (this.isAllSelected) {
|
|
|
+ this.selectedSongs = [];
|
|
|
+ this.isAllSelected = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.isMultiSelect) {
|
|
|
+ this.isMultiSelect = true;
|
|
|
+ }
|
|
|
+ this.selectedSongs = this.songs.slice();
|
|
|
+ this.isAllSelected = this.selectedSongs.length > 0 && this.selectedSongs.length === this.songs.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ private exitMultiSelect(): void {
|
|
|
+ this.isMultiSelect = false;
|
|
|
+ this.selectedSongs = [];
|
|
|
+ this.isAllSelected = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private syncSelectionAfterRefresh(): void {
|
|
|
+ if (!this.isMultiSelect) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const currentKeys: Set<string> = new Set(this.songs.map(item => item.filePath));
|
|
|
+ const filtered = this.selectedSongs.filter(item => currentKeys.has(item.filePath));
|
|
|
+ if (filtered.length !== this.selectedSongs.length) {
|
|
|
+ this.selectedSongs = filtered;
|
|
|
+ }
|
|
|
+ this.isAllSelected = filtered.length > 0 && filtered.length === this.songs.length;
|
|
|
+ if (filtered.length === 0) {
|
|
|
+ this.exitMultiSelect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private confirmDeleteSelected(): void {
|
|
|
+ if (!this.selectedAccount) {
|
|
|
+ this.getUIContext().getPromptAction().showToast({ message: `请先选择${getRemoteDriveAccountLabel()}` });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.isMultiSelect || this.selectedSongs.length === 0) {
|
|
|
+ this.getUIContext().getPromptAction().showToast({ message: '请先选择要删除的文件' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.getUIContext().showAlertDialog({
|
|
|
+ title: '删除文件',
|
|
|
+ message: `确定删除选中的${this.selectedSongs.length}个文件吗?`,
|
|
|
+ primaryButton: {
|
|
|
+ value: '取消',
|
|
|
+ action: () => {}
|
|
|
+ },
|
|
|
+ secondaryButton: {
|
|
|
+ value: '删除',
|
|
|
+ fontColor: Color.Red,
|
|
|
+ action: () => {
|
|
|
+ void this.performDeleteSelected();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private async performDeleteSelected(): Promise<void> {
|
|
|
+ if (!this.selectedAccount || this.selectedSongs.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.isDeletingSelection = true;
|
|
|
+ try {
|
|
|
+ await this.webdavManager.deleteRemoteSongs(this.selectedAccount, this.selectedSongs.slice());
|
|
|
+ await this.webdavManager.loadFilesInfoFromAccount(this.selectedAccount, this.webdavManager.currentPath);
|
|
|
+ this.exitMultiSelect();
|
|
|
+ this.getUIContext().getPromptAction().showToast({ message: '删除成功' });
|
|
|
+ } catch (error) {
|
|
|
+ const err = error as Error;
|
|
|
+ this.getUIContext().getPromptAction().showToast({ message: `删除失败: ${err.message}` });
|
|
|
+ } finally {
|
|
|
+ this.isDeletingSelection = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private buildSongMetaLine(song: VideoItem): string {
|
|
|
const parts: string[] = [];
|
|
|
if (StrUtil.isNotEmpty(song.duration)) {
|
|
|
@@ -238,6 +368,7 @@ export struct WebDavMainPage {
|
|
|
// 更新可见文件夹列表
|
|
|
this.updateVisibleFolders();
|
|
|
this.breadcrumbs = this.webdavManager.getBreadcrumbs();
|
|
|
+ this.syncSelectionAfterRefresh();
|
|
|
|
|
|
// promptAction.showToast({
|
|
|
// message: '加载成功: ' + this.webDavFiles.filter(f => f.isDirectory).length + '个文件夹, ' + this.songs.length + '首歌曲'
|
|
|
@@ -945,7 +1076,7 @@ export struct WebDavMainPage {
|
|
|
|
|
|
}
|
|
|
}
|
|
|
- .padding({ top: this.topSafeHeight+10, left: 10, right: 10,bottom:2 })
|
|
|
+ .padding({ top: this.topSafeHeight + 10, left: 10, right: 10, bottom: 2 })
|
|
|
.width('100%')
|
|
|
}
|
|
|
|
|
|
@@ -994,22 +1125,30 @@ export struct WebDavMainPage {
|
|
|
} else {
|
|
|
this.buildContentView();
|
|
|
}
|
|
|
+
|
|
|
+ // 顶部区域:标题栏在上,面包屑在下
|
|
|
Column() {
|
|
|
this.topTitleBar()
|
|
|
this.breaker()
|
|
|
}
|
|
|
+ .width('100%')
|
|
|
+ .position({ top: 0, left: 0 })
|
|
|
+ .backgroundColor($r('app.color.start_window_background'))
|
|
|
|
|
|
+ // 多选操作条,位于播放条之上(仅在多选模式下显示)
|
|
|
+ if (this.isMultiSelect || this.isDeletingSelection) {
|
|
|
+ this.buildSelectionOverlay()
|
|
|
+ }
|
|
|
}
|
|
|
- .alignContent(Alignment.Top)
|
|
|
.width('100%')
|
|
|
.height('100%')
|
|
|
+ .alignContent(Alignment.Bottom)
|
|
|
}
|
|
|
|
|
|
@Builder
|
|
|
breaker() {
|
|
|
- // 加载按钮和面包屑导航
|
|
|
+ // 面包屑导航
|
|
|
Column({ space: 8 }) {
|
|
|
-
|
|
|
// 面包屑导航
|
|
|
if (this.webdavManager.currentPath !== '') {
|
|
|
Row({ space: 8 }) {
|
|
|
@@ -1058,7 +1197,7 @@ export struct WebDavMainPage {
|
|
|
|
|
|
}
|
|
|
.width('100%')
|
|
|
- .padding({ left: 12, right: 12,top: 10,bottom: 5 })
|
|
|
+ .padding({ left: 12, right: 12, top: 8, bottom: 5 })
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1133,7 +1272,7 @@ export struct WebDavMainPage {
|
|
|
}, (item: VideoItem) => item.filePath + '_' + this.listRefreshKey)
|
|
|
}
|
|
|
.scrollBar(BarState.Off)
|
|
|
- .contentStartOffset(this.topSafeHeight + 80)
|
|
|
+ .contentStartOffset(this.topSafeHeight + 110)
|
|
|
.contentEndOffset(this.bottomSafeHeight+70)
|
|
|
.layoutWeight(1)
|
|
|
.margin({ top: 4 })
|
|
|
@@ -1157,6 +1296,65 @@ export struct WebDavMainPage {
|
|
|
|
|
|
}
|
|
|
|
|
|
+ @Builder
|
|
|
+ private buildMultiSelectBar() {
|
|
|
+ Row({ space: 12 }) {
|
|
|
+ Button(this.isAllSelected ? '反选' : '全选', { type: ButtonType.Circle, stateEffect: true })
|
|
|
+ .width(60)
|
|
|
+ .height(40)
|
|
|
+ .fontSize(12)
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
|
|
|
+ .onClick(() => this.toggleSelectAll())
|
|
|
+
|
|
|
+ Button('删除', { type: ButtonType.Circle, stateEffect: true })
|
|
|
+ .width(60)
|
|
|
+ .height(40)
|
|
|
+ .fontSize(12)
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
|
|
|
+ .enabled(this.selectedSongs.length > 0 && !this.isDeletingSelection)
|
|
|
+ .onClick(() => this.confirmDeleteSelected())
|
|
|
+
|
|
|
+ Button('取消', { type: ButtonType.Circle, stateEffect: true })
|
|
|
+ .width(60)
|
|
|
+ .height(40)
|
|
|
+ .fontSize(12)
|
|
|
+ .backgroundColor(this.themeColor)
|
|
|
+ .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.5 })
|
|
|
+ .onClick(() => this.exitMultiSelect())
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .padding({ bottom: this.bottomSafeHeight + 10, top: 12 })
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
+ .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
|
|
|
+ .opacity(this.isMultiSelect ? 1 : 0)
|
|
|
+ .animation({ duration: 300, curve: 'ease-in-out' })
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ private buildSelectionOverlay() {
|
|
|
+ Column() {
|
|
|
+ Row({ space: 10 }) {
|
|
|
+ LoadingProgress()
|
|
|
+ .width(22)
|
|
|
+ .height(22)
|
|
|
+ .color(this.themeColor)
|
|
|
+ Text('正在删除...')
|
|
|
+ .fontSize(14)
|
|
|
+ .fontColor($r('app.color.index_tab_font_color'))
|
|
|
+ }
|
|
|
+ .padding({ top: 12, bottom: 6 })
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
+ .visibility(this.isDeletingSelection ? Visibility.Visible : Visibility.None)
|
|
|
+
|
|
|
+ this.buildMultiSelectBar()
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .padding({ left: 12, right: 12, bottom: this.bottomSafeHeight + 120 })
|
|
|
+ .backgroundColor($r('app.color.start_window_background'))
|
|
|
+ }
|
|
|
+
|
|
|
// 文件夹列表项
|
|
|
@Builder
|
|
|
buildFolderItem(folder: FileInfo) {
|
|
|
@@ -1219,7 +1417,11 @@ export struct WebDavMainPage {
|
|
|
})
|
|
|
.margin({ left: 8 })
|
|
|
.onClick(() => {
|
|
|
- this.playSong(song, index, true);
|
|
|
+ if (this.isMultiSelect) {
|
|
|
+ this.toggleSongSelection(song);
|
|
|
+ } else {
|
|
|
+ this.playSong(song, index, true);
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
// 歌曲信息
|
|
|
@@ -1254,22 +1456,19 @@ export struct WebDavMainPage {
|
|
|
.padding({ right: 20 })
|
|
|
|
|
|
Column() {
|
|
|
- //多选按钮的Checkbox 先注释掉
|
|
|
- // Checkbox({ name: 'checkbox' + index })
|
|
|
- // .select(this.selectedFiles.some(x => x.filePath === item.filePath))
|
|
|
- // .selectedColor(this.themeColor)
|
|
|
- // .shape(CheckBoxShape.CIRCLE)
|
|
|
- // .opacity(this.isMultiSelect ? 1 : 0)
|
|
|
- // .animation({
|
|
|
- // duration: 666,
|
|
|
- // curve: 'Smooth' // 可选动画曲线
|
|
|
- // })
|
|
|
- // .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
|
|
|
- // .onChange((checked: boolean) => this.handleFileSelection(item, checked))
|
|
|
- // .margin({ left: 20, top: 8, bottom: 8,right:18 })
|
|
|
- // .width(22)
|
|
|
- // .height(22)
|
|
|
-
|
|
|
+ Checkbox({ name: 'checkbox_' + index })
|
|
|
+ .select(this.isSongSelected(song))
|
|
|
+ .selectedColor(this.themeColor)
|
|
|
+ .opacity(this.isMultiSelect ? 1 : 0)
|
|
|
+ .animation({
|
|
|
+ duration: 300,
|
|
|
+ curve: 'Smooth'
|
|
|
+ })
|
|
|
+ .visibility(this.isMultiSelect ? Visibility.Visible : Visibility.None)
|
|
|
+ .onChange((checked: boolean) => this.handleCheckboxSelection(song, checked))
|
|
|
+ .margin({ left: 10, top: 8, bottom: 8, right: 12 })
|
|
|
+ .width(22)
|
|
|
+ .height(22)
|
|
|
|
|
|
ImageAnimator()
|
|
|
.images(CommonConstants.IMAGE_FRAME_INFO)// 动画数组
|
|
|
@@ -1279,7 +1478,7 @@ export struct WebDavMainPage {
|
|
|
.fillMode(FillMode.Forwards)
|
|
|
.width(18)
|
|
|
.margin({ right: 12, top: 8, bottom: 8 })
|
|
|
- .visibility(this.currentSong?.filePath==song.filePath ? Visibility.Visible :
|
|
|
+ .visibility(this.currentSong?.filePath==song.filePath ? (this.isMultiSelect ? Visibility.None : Visibility.Visible) :
|
|
|
Visibility.None)
|
|
|
.height(18)
|
|
|
.iterations(-1) // 播放次数
|
|
|
@@ -1291,7 +1490,14 @@ export struct WebDavMainPage {
|
|
|
.clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
|
|
|
.backgroundColor(Color.Transparent)
|
|
|
.onClick(() => {
|
|
|
- this.playSong(song, index);
|
|
|
+ if (this.isMultiSelect) {
|
|
|
+ this.toggleSongSelection(song);
|
|
|
+ } else {
|
|
|
+ this.playSong(song, index);
|
|
|
+ }
|
|
|
})
|
|
|
+ .gesture(LongPressGesture().onAction(() => {
|
|
|
+ this.enterMultiSelect(song);
|
|
|
+ }))
|
|
|
}
|
|
|
}
|