Prechádzať zdrojové kódy

feat(card): add music card manager and stores

onecold 4 mesiacov pred
rodič
commit
4240002f59

+ 140 - 0
entry/src/main/ets/common/player/MusicCardFormStore.ets

@@ -0,0 +1,140 @@
+import data_preferences from '@ohos.data.preferences'
+import { common } from '@kit.AbilityKit'
+import Logger from '../util/Logger'
+
+const TAG = 'MusicCardFormStore'
+const STORE_NAME = 'music_card_form_store'
+const FORM_IDS_KEY = 'music_card_form_ids'
+type Preferences = ReturnType<typeof data_preferences.getPreferencesSync>
+
+const formatError = (error: unknown): string => {
+  if (error instanceof Error) {
+    return error.message
+  }
+  return `${error}`
+}
+
+const resolveContext = (context?: common.Context): common.Context | undefined => {
+  if (context) {
+    return context
+  }
+  return AppStorage.get('context') as common.Context | undefined
+}
+
+const resolvePreferences = (context?: common.Context): Preferences | undefined => {
+  const resolvedContext = resolveContext(context)
+  if (!resolvedContext) {
+    Logger.warn(TAG, 'context is undefined')
+    return undefined
+  }
+  try {
+    return data_preferences.getPreferencesSync(resolvedContext, STORE_NAME)
+  } catch (error) {
+    Logger.error(TAG, `getPreferencesSync failed: ${formatError(error)}`)
+    return undefined
+  }
+}
+
+const normalizeFormId = (value: unknown): number | null => {
+  if (typeof value === 'number' && Number.isFinite(value)) {
+    return value
+  }
+  if (typeof value === 'string') {
+    const parsed = Number(value)
+    if (Number.isFinite(parsed)) {
+      return parsed
+    }
+  }
+  return null
+}
+
+const readFormIdsFromPreferences = (preferences: Preferences): number[] => {
+  let raw = ''
+  try {
+    raw = preferences.getSync(FORM_IDS_KEY, '') as string
+  } catch (error) {
+    Logger.error(TAG, `readFormIds failed: ${formatError(error)}`)
+    return []
+  }
+  if (!raw || raw.length === 0) {
+    return []
+  }
+  try {
+    const parsed = JSON.parse(raw) as Array<number | string>
+    if (!Array.isArray(parsed)) {
+      return []
+    }
+    const result: number[] = []
+    for (let i = 0; i < parsed.length; i++) {
+      const normalized = normalizeFormId(parsed[i])
+      if (normalized !== null) {
+        result.push(normalized)
+      }
+    }
+    return Array.from(new Set(result))
+  } catch (error) {
+    Logger.error(TAG, `parseFormIds failed: ${formatError(error)}`)
+    return []
+  }
+}
+
+const writeFormIdsToPreferences = (
+  preferences: Preferences,
+  formIds: number[]
+): void => {
+  const payload = JSON.stringify(formIds ?? [])
+  preferences.putSync(FORM_IDS_KEY, payload)
+  preferences.flushSync()
+}
+
+export class MusicCardFormStore {
+  static readFormIds(context?: common.Context): number[] {
+    const preferences = resolvePreferences(context)
+    if (!preferences) {
+      return []
+    }
+    return readFormIdsFromPreferences(preferences)
+  }
+
+  static addFormId(context: common.Context | undefined, formId: number): void {
+    const preferences = resolvePreferences(context)
+    if (!preferences) {
+      return
+    }
+    const normalized = normalizeFormId(formId)
+    if (normalized === null) {
+      return
+    }
+    const current = readFormIdsFromPreferences(preferences)
+    if (current.includes(normalized)) {
+      return
+    }
+    current.push(normalized)
+    try {
+      writeFormIdsToPreferences(preferences, current)
+    } catch (error) {
+      Logger.error(TAG, `addFormId failed: ${formatError(error)}`)
+    }
+  }
+
+  static removeFormId(context: common.Context | undefined, formId: number): void {
+    const preferences = resolvePreferences(context)
+    if (!preferences) {
+      return
+    }
+    const normalized = normalizeFormId(formId)
+    if (normalized === null) {
+      return
+    }
+    const current = readFormIdsFromPreferences(preferences)
+    const next = current.filter((item) => item !== normalized)
+    if (next.length === current.length) {
+      return
+    }
+    try {
+      writeFormIdsToPreferences(preferences, next)
+    } catch (error) {
+      Logger.error(TAG, `removeFormId failed: ${formatError(error)}`)
+    }
+  }
+}

+ 111 - 0
entry/src/main/ets/common/player/MusicCardManager.ets

@@ -0,0 +1,111 @@
+import { common } from '@kit.AbilityKit'
+import Logger from '../util/Logger'
+import { VideoItem } from '../../viewmodel/VideoItem'
+import {
+  createEmptyMusicCardSnapshot,
+  MusicCardSnapshot,
+  resolveMusicCardLyricLines
+} from './MusicCardSnapshot'
+import { MusicCardSnapshotStore } from './MusicCardSnapshotStore'
+
+const TAG = 'MusicCardManager'
+const DEFAULT_TIME_TEXT = '00:00'
+
+const padTime = (value: number): string => {
+  return value < 10 ? `0${value}` : `${value}`
+}
+
+const formatTime = (timeMs: number): string => {
+  if (!Number.isFinite(timeMs) || timeMs <= 0) {
+    return DEFAULT_TIME_TEXT
+  }
+  const totalSeconds = Math.floor(timeMs / 1000)
+  const seconds = totalSeconds % 60
+  const minutes = Math.floor(totalSeconds / 60) % 60
+  const hours = Math.floor(totalSeconds / 3600)
+  if (hours > 0) {
+    return `${padTime(hours)}:${padTime(minutes)}:${padTime(seconds)}`
+  }
+  return `${padTime(minutes)}:${padTime(seconds)}`
+}
+
+export class MusicCardManager {
+  private static instance: MusicCardManager
+
+  static getInstance(): MusicCardManager {
+    if (!MusicCardManager.instance) {
+      MusicCardManager.instance = new MusicCardManager()
+    }
+    return MusicCardManager.instance
+  }
+
+  static buildSnapshotForTest(
+    song?: VideoItem,
+    isPlaying: boolean,
+    positionMs: number,
+    durationMs: number,
+    coverPath: string
+  ): MusicCardSnapshot {
+    if (!song) {
+      const emptySnapshot = createEmptyMusicCardSnapshot()
+      emptySnapshot.updatedAtMs = Date.now()
+      return emptySnapshot
+    }
+    const snapshot = createEmptyMusicCardSnapshot()
+    snapshot.hasSong = true
+    snapshot.title = song.name || song.fileName || ''
+    snapshot.artist = song.artist ?? ''
+    snapshot.coverPath = coverPath ?? ''
+    snapshot.hasCoverImage = !!snapshot.coverPath
+    snapshot.isPlaying = Boolean(isPlaying)
+    snapshot.currentPositionMs = positionMs ?? 0
+    snapshot.durationMs = durationMs ?? 0
+    snapshot.currentTimeText = formatTime(snapshot.currentPositionMs)
+    snapshot.durationTimeText = formatTime(snapshot.durationMs)
+    snapshot.filePath = song.filePath ?? ''
+    snapshot.lyricText = song.lyricContent ?? ''
+    snapshot.updatedAtMs = Date.now()
+
+    const lyricLines = resolveMusicCardLyricLines(snapshot.lyricText, snapshot.currentPositionMs)
+    if (lyricLines.hasLyric) {
+      snapshot.lyricLine1 = lyricLines.line1
+      snapshot.lyricLine2 = lyricLines.line2
+      snapshot.hasLyric = true
+    } else {
+      snapshot.lyricLine1 = snapshot.title
+      snapshot.lyricLine2 = snapshot.artist
+      snapshot.hasLyric = false
+    }
+
+    return snapshot
+  }
+
+  static shouldUpdateLyricCardForTest(
+    lastUpdateMs: number,
+    currentPositionMs: number,
+    thresholdMs: number
+  ): boolean {
+    if (!Number.isFinite(thresholdMs) || thresholdMs <= 0) {
+      return true
+    }
+    if (!Number.isFinite(lastUpdateMs) || !Number.isFinite(currentPositionMs)) {
+      return true
+    }
+    return Math.abs(currentPositionMs - lastUpdateMs) >= thresholdMs
+  }
+
+  notifyPlaybackStateChanged(
+    context: common.Context | undefined,
+    snapshot: MusicCardSnapshot
+  ): void {
+    if (!snapshot) {
+      Logger.warn(TAG, 'notifyPlaybackStateChanged skipped: snapshot is undefined')
+      return
+    }
+    snapshot.updatedAtMs = Date.now()
+    MusicCardSnapshotStore.writeSnapshot(snapshot, context)
+    this.updateAllForms(context)
+  }
+
+  updateAllForms(_context?: common.Context): void {}
+}

+ 79 - 0
entry/src/main/ets/common/player/MusicCardSnapshotStore.ets

@@ -0,0 +1,79 @@
+import data_preferences from '@ohos.data.preferences'
+import { common } from '@kit.AbilityKit'
+import Logger from '../util/Logger'
+import { createEmptyMusicCardSnapshot, MusicCardSnapshot } from './MusicCardSnapshot'
+
+const TAG = 'MusicCardSnapshotStore'
+const STORE_NAME = 'music_card_snapshot_store'
+const SNAPSHOT_KEY = 'music_card_snapshot'
+type Preferences = ReturnType<typeof data_preferences.getPreferencesSync>
+
+const formatError = (error: unknown): string => {
+  if (error instanceof Error) {
+    return error.message
+  }
+  return `${error}`
+}
+
+const resolveContext = (context?: common.Context): common.Context | undefined => {
+  if (context) {
+    return context
+  }
+  return AppStorage.get('context') as common.Context | undefined
+}
+
+const resolvePreferences = (context?: common.Context): Preferences | undefined => {
+  const resolvedContext = resolveContext(context)
+  if (!resolvedContext) {
+    Logger.warn(TAG, 'context is undefined')
+    return undefined
+  }
+  try {
+    return data_preferences.getPreferencesSync(resolvedContext, STORE_NAME)
+  } catch (error) {
+    Logger.error(TAG, `getPreferencesSync failed: ${formatError(error)}`)
+    return undefined
+  }
+}
+
+export class MusicCardSnapshotStore {
+  static readSnapshot(context?: common.Context): MusicCardSnapshot {
+    const preferences = resolvePreferences(context)
+    if (!preferences) {
+      return createEmptyMusicCardSnapshot()
+    }
+    let raw = ''
+    try {
+      raw = preferences.getSync(SNAPSHOT_KEY, '') as string
+    } catch (error) {
+      Logger.error(TAG, `readSnapshot failed: ${formatError(error)}`)
+      return createEmptyMusicCardSnapshot()
+    }
+    if (!raw || raw.length === 0) {
+      return createEmptyMusicCardSnapshot()
+    }
+    try {
+      const parsed = JSON.parse(raw) as MusicCardSnapshot
+      return { ...createEmptyMusicCardSnapshot(), ...parsed }
+    } catch (error) {
+      Logger.error(TAG, `parseSnapshot failed: ${formatError(error)}`)
+      return createEmptyMusicCardSnapshot()
+    }
+  }
+
+  static writeSnapshot(snapshot: MusicCardSnapshot, context?: common.Context): boolean {
+    const preferences = resolvePreferences(context)
+    if (!preferences) {
+      return false
+    }
+    try {
+      const payload = JSON.stringify(snapshot ?? createEmptyMusicCardSnapshot())
+      preferences.putSync(SNAPSHOT_KEY, payload)
+      preferences.flushSync()
+      return true
+    } catch (error) {
+      Logger.error(TAG, `writeSnapshot failed: ${formatError(error)}`)
+      return false
+    }
+  }
+}

+ 2 - 0
entry/src/ohosTest/ets/test/List.test.ets

@@ -21,6 +21,7 @@ import playbackCoordinatorTest from './PlaybackCoordinator.test'
 import playbackStateBridgeTest from './PlaybackStateBridge.test'
 import remoteMusicViewModeHelperTest from './RemoteMusicViewModeHelper.test'
 import musicCardSnapshotTest from './MusicCardSnapshot.test'
+import musicCardManagerTest from './MusicCardManager.test'
 
 export default function testsuite() {
   abilityTest()
@@ -31,4 +32,5 @@ export default function testsuite() {
   playbackStateBridgeTest()
   remoteMusicViewModeHelperTest()
   musicCardSnapshotTest()
+  musicCardManagerTest()
 }

+ 38 - 0
entry/src/ohosTest/ets/test/MusicCardManager.test.ets

@@ -0,0 +1,38 @@
+import { describe, it, expect } from '@ohos/hypium'
+import { MusicCardManager } from '../../../main/ets/common/player/MusicCardManager'
+import { VideoItem } from '../../../main/ets/viewmodel/VideoItem'
+
+export default function musicCardManagerTest() {
+  describe('MusicCardManagerTest', () => {
+    it('buildSnapshotForTestDerivesSnapshotFields', 0, () => {
+      const song = new VideoItem('夜曲', '1', '/storage/track.mp3', 0, 0, '')
+      song.artist = '周杰伦'
+      song.lyricContent = '[00:00.00]第一句\n[00:03.00]第二句\n[00:05.00]周杰伦'
+
+      const snapshot = MusicCardManager.buildSnapshotForTest(
+        song,
+        true,
+        3500,
+        180000,
+        '/cache/cover.jpg'
+      )
+
+      expect(snapshot.title).assertEqual('夜曲')
+      expect(snapshot.artist).assertEqual('周杰伦')
+      expect(snapshot.lyricLine1).assertEqual('第二句')
+      expect(snapshot.lyricLine2).assertEqual('周杰伦')
+      expect(snapshot.hasSong).assertEqual(true)
+      expect(snapshot.isPlaying).assertEqual(true)
+    })
+
+    it('shouldUpdateLyricCardReturnsFalseWhenWithinThreshold', 0, () => {
+      const result = MusicCardManager.shouldUpdateLyricCardForTest(1000, 1500, 800)
+      expect(result).assertEqual(false)
+    })
+
+    it('shouldUpdateLyricCardReturnsTrueWhenBeyondThreshold', 0, () => {
+      const result = MusicCardManager.shouldUpdateLyricCardForTest(1000, 2100, 800)
+      expect(result).assertEqual(true)
+    })
+  })
+}