iT邦幫忙

2025 iThome 鐵人賽

DAY 28
1

還剩兩天鐵人賽就結束啦,這次來做筆記頁的子功能

AI 寫扣經過

  1. NoteEditScreen 增加 UI
    // Function Panel
    LazyRow(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        item { IconButton(onClick = { /* TODO: Undo */ }) { Icon(Icons.Default.Undo, contentDescription = "復原") } }
        item { IconButton(onClick = { /* TODO: Redo */ }) { Icon(Icons.Default.Redo, contentDescription = "重做") } }
        item { IconButton(onClick = onPlayTitle) { Icon(Icons.Default.PlayArrow, contentDescription = "朗讀標題") } }
        item { IconButton(onClick = onPlayContent) { Icon(Icons.Default.PlayArrow, contentDescription = "朗讀內容") } }
        item { IconButton(onClick = { /* TODO: Delete */ }) { Icon(Icons.Default.Delete, contentDescription = "刪除筆記") } }
        item { IconButton(onClick = { /* TODO: Custom Color */ }) { Icon(Icons.Default.Edit, contentDescription = "自訂顏色") } }
        // ... Add other buttons here
    }
    
  2. 文字轉語音 (TTS) 功能,建立 TextToSpeechHelper
    @Singleton
    class TextToSpeechHelper @Inject constructor(
        @ApplicationContext private val context: Context
    ) : TextToSpeech.OnInitListener {
    
        private var tts: TextToSpeech? = null
        private val _isInitialized = MutableStateFlow(false)
        val isInitialized = _isInitialized.asStateFlow()
    
        init {
            try {
                tts = TextToSpeech(context, this)
            } catch (e: Exception) {
                Timber.e(e, "Failed to initialize TextToSpeech")
            }
        }
    
        override fun onInit(status: Int) {
            if (status == TextToSpeech.SUCCESS) {
                val result = tts?.setLanguage(Locale.TRADITIONAL_CHINESE)
                if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                    Timber.e("TTS language (Traditional Chinese) not supported.")
                    _isInitialized.value = false
                } else {
                    _isInitialized.value = true
                    Timber.d("TTS initialized successfully.")
                }
            } else {
                Timber.e("TTS initialization failed with status: $status")
                _isInitialized.value = false
            }
        }
    
        fun speak(text: String) {
            if (_isInitialized.value && tts != null) {
                // Handle long text by splitting it
                val maxLength = TextToSpeech.getMaxSpeechInputLength()
                if (text.length > maxLength) {
                    val chunks = text.chunked(maxLength)
                    chunks.forEachIndexed { index, chunk ->
                        val utteranceId = "${System.currentTimeMillis()}_$index"
                        tts?.speak(chunk, TextToSpeech.QUEUE_ADD, null, utteranceId)
                    }
                } else {
                    val utteranceId = "${System.currentTimeMillis()}"
                    tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId)
                }
            } else {
                Timber.w("TTS not initialized or available, cannot speak.")
            }
        }
    
        fun stop() {
            tts?.stop()
        }
    
        fun shutdown() {
            if (tts != null) {
                tts?.stop()
                tts?.shutdown()
                tts = null
                _isInitialized.value = false
                Timber.d("TTS shutdown.")
            }
        }
    }
    
  3. 更新 NoteEditViewModel
    fun playTitle() {
        _uiState.value.note?.title?.let {
            if (it.isNotBlank()) {
                ttsHelper.speak(it)
            }
        }
    }
    fun playContent() {
        _uiState.value.note?.content?.let {
            if (it.isNotBlank()) {
                ttsHelper.speak(it)
            }
        }
    }
    
  4. 更新 NoteEditScreen UI
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun NoteEditScreenContent(
        modifier: Modifier = Modifier,
        uiState: NoteEditUiState,
        onSaveNote: (String, String) -> Unit,
        onPlayTitle: () -> Unit,
        onPlayContent: () -> Unit,
        onNavigateBack: () -> Unit
    ) {
        var title by remember(uiState.note?.title) { mutableStateOf(uiState.note?.title ?: "") }
        var content by remember(uiState.note?.content) { mutableStateOf(uiState.note?.content ?: "") }
    
        Scaffold(
            modifier = modifier.fillMaxSize(),
            topBar = {
                TopAppBar(
                    title = { Text(if (uiState.note?.id == 0L) "新增筆記" else "編輯筆記") },
                    navigationIcon = {
                        IconButton(onClick = onNavigateBack) {
                            Icon(
                                imageVector = Icons.Default.ArrowBack,
                                contentDescription = "返回"
                            )
                        }
                    },
                    actions = {
                        IconButton(onClick = { onSaveNote(title, content) }) {
                            Icon(
                                imageVector = Icons.Default.Done,
                                contentDescription = "儲存"
                            )
                        }
                    }
                )
            }
        ) { innerPadding ->
            if (uiState.isLoading) {
                CircularProgressIndicator()
            } else {
                Column(
                    modifier = Modifier
                        .padding(innerPadding)
                        .fillMaxSize()
                ) {
                    TextField(
                        value = title,
                        onValueChange = { title = it },
                        label = { Text("標題") },
                        modifier = Modifier.fillMaxWidth()
                    )
                    TextField(
                        value = content,
                        onValueChange = { content = it },
                        label = { Text("內容") },
                        modifier = Modifier
                            .fillMaxWidth()
                            .weight(1f)
                    )
    
                    // Function Panel
                    LazyRow(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        item { IconButton(onClick = { /* TODO: Undo */ }) { Icon(Icons.Default.Undo, contentDescription = "復原") } }
                        item { IconButton(onClick = { /* TODO: Redo */ }) { Icon(Icons.Default.Redo, contentDescription = "重做") } }
                        item { IconButton(onClick = onPlayTitle) { Icon(Icons.Default.PlayArrow, contentDescription = "朗讀標題") } }
                        item { IconButton(onClick = onPlayContent) { Icon(Icons.Default.PlayArrow, contentDescription = "朗讀內容") } }
                        item { IconButton(onClick = { /* TODO: Delete */ }) { Icon(Icons.Default.Delete, contentDescription = "刪除筆記") } }
                        item { IconButton(onClick = { /* TODO: Custom Color */ }) { Icon(Icons.Default.Edit, contentDescription = "自訂顏色") } }
                        // ... Add other buttons here
                    }
                }
            }
        }
    }
    

上一篇
114/27 - Vibe Coding 建立分類功能
下一篇
114/29 - Vibe Coding 游標控制和剪貼簿功能
系列文
看見筆記捲土重來,試著用 Vibe Coding 完成一款 App 吧!29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
chiaominchang222
iT邦新手 5 級 ‧ 2025-10-12 22:29:39

我剩三天嗚嗚

我要留言

立即登入留言