From cf2117beab56aaa0a28901e49eeac1dd9e2b4598 Mon Sep 17 00:00:00 2001 From: thcathy Date: Sun, 4 Oct 2026 21:42:53 +0800 Subject: [PATCH 1/4] Replace search filters with chips and a labeled search field. Enter runs the same search as the button when the form is valid. Suitable, type, and date stay visible as chips that open a popover, and Created By sits above them. The dictation list preview is the last line of each row. Co-authored-by: Cursor --- e2e/search-dictation-open-by-id.spec.ts | 2 +- .../dictation-list/dictation-list.html | 12 +-- .../dictation-list/dictation-list.spec.ts | 3 +- .../search-dictation.page.html | 92 ++++++++++-------- .../search-dictation.page.scss | 43 +++++++- .../search-dictation.page.spec.ts | 97 +++++++++++++++++++ .../search-dictation/search-dictation.page.ts | 57 ++++++++++- src/assets/i18n/en.json | 3 +- src/assets/i18n/zh-Hans.json | 2 +- src/assets/i18n/zh-Hant.json | 2 +- 10 files changed, 256 insertions(+), 57 deletions(-) diff --git a/e2e/search-dictation-open-by-id.spec.ts b/e2e/search-dictation-open-by-id.spec.ts index b4025501..fd38681c 100644 --- a/e2e/search-dictation-open-by-id.spec.ts +++ b/e2e/search-dictation-open-by-id.spec.ts @@ -10,7 +10,7 @@ test.describe('search dictation opens by id', () => { test('view the first search result on /dictation-view/:id', async ({ page }) => { await page.goto('/search-dictation'); - const keyword = page.locator('ion-searchbar input'); + const keyword = page.locator('ion-input[formcontrolname="keyword"] input'); await keyword.click(); await keyword.fill('apple'); diff --git a/src/app/components/dictation-list/dictation-list.html b/src/app/components/dictation-list/dictation-list.html index e5b4a2ee..2e4023e3 100644 --- a/src/app/components/dictation-list/dictation-list.html +++ b/src/app/components/dictation-list/dictation-list.html @@ -21,12 +21,6 @@ {{dictation.title}} {{ dictation.createdDate | date: 'yyyy-MM-dd' }} - @let line = previewLine(dictation); - - -
{{ line }}
-
-
{{ dictation | dictationQuestions }} @@ -49,6 +43,12 @@ {{ 'Suitable' | translate }}: {{ 'SuitableStudent.' + dictation.suitableStudent | translate }} } + @let line = previewLine(dictation); + + +
{{ line }}
+
+
{{ 'ViewDictation' | translate }} diff --git a/src/app/components/dictation-list/dictation-list.spec.ts b/src/app/components/dictation-list/dictation-list.spec.ts index 8c45bd59..a61dbf77 100644 --- a/src/app/components/dictation-list/dictation-list.spec.ts +++ b/src/app/components/dictation-list/dictation-list.spec.ts @@ -48,7 +48,7 @@ describe('DictationListComponent', () => { expect(component.navService.openDictationById).not.toHaveBeenCalled(); }); - it('renders one preview line under the heading on every row', () => { + it('renders one preview line at the bottom of every row', () => { const described = TestData.fillInDictation(); described.description = 'Fruit words'; described.suitableStudent = 'Kindergarten'; @@ -67,6 +67,7 @@ describe('DictationListComponent', () => { expect(previews.length).toBe(1); const heading = label.querySelector('.heading'); expect(heading.compareDocumentPosition(previews[0]) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(label.lastElementChild.classList.contains('preview-row')).toBeTrue(); }); expect(labels[0].querySelector('.preview-line').textContent.trim()).toBe('Fruit words'); expect(labels[0].querySelector('.preview-line').getAttribute('title')).toBe('Fruit words'); diff --git a/src/app/pages/search-dictation/search-dictation.page.html b/src/app/pages/search-dictation/search-dictation.page.html index 1a734ea0..da20d481 100644 --- a/src/app/pages/search-dictation/search-dictation.page.html +++ b/src/app/pages/search-dictation/search-dictation.page.html @@ -10,8 +10,10 @@
- - @if (filteredHistory && showHistory) { + + + + @if (showHistory && filteredHistory.length) { @for (h of filteredHistory; track h) { {{h}} @@ -20,7 +22,7 @@ } @if (keyword.invalid && (keyword.dirty || keyword.touched)) {
- @if (keyword.errors.minlength) { + @if (keyword.errors.pattern) {
{{'CharactersTooShort'|translate:{length: '3'} }}
} @if (keyword.errors.maxlength) { @@ -28,11 +30,9 @@ }
} - @if (moreOptions) { - - - - } + + + @if (creator.invalid && (creator.dirty || creator.touched)) {
@if (creator.errors.minlength) { @@ -43,42 +43,52 @@ }
} - @if (moreOptions) { - - +
+ + + +
+ + + @for (state of suitableStudentOptions; track state) { - {{'SuitableStudent.' + state|translate}} + {{'SuitableStudent.' + state|translate}} } -
-
- } - @if (moreOptions) { - - - {{'Any'|translate}} - {{'Word'|translate}} - {{'Sentence'|translate}} - - - } - @if (moreOptions) { - - - @for (option of dateOptions; track option) { - {{option.option|translate}} +
+ + + + + + @for (option of typeOptions; track option.value) { + {{option.label|translate}} } - - - } - - - {{'More Options'|translate}} - - - -  {{'Search'|translate}} - + + + + + + + @for (option of dateOptions; track option.option) { + {{option.option|translate}} + } + + + +
+ +  {{'Search'|translate}} + +
diff --git a/src/app/pages/search-dictation/search-dictation.page.scss b/src/app/pages/search-dictation/search-dictation.page.scss index 219f1148..b5c4f1f5 100644 --- a/src/app/pages/search-dictation/search-dictation.page.scss +++ b/src/app/pages/search-dictation/search-dictation.page.scss @@ -1,5 +1,9 @@ +ion-input { + --placeholder-color: #a0a0a0; +} + .autocomplete { - width: calc(100% - 48px); + width: 100%; position: absolute; z-index: 999; background: #f1f1f1; @@ -34,3 +38,40 @@ ion-button { .error-text { padding-left: 16px; } + +.filters { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + margin: 12px 16px; +} + +.filter-chip { + display: inline-flex; + align-items: center; + flex: 0 0 auto; + border: 0; + border-radius: 16px; + margin: 0; + padding: 0 10px; + height: 32px; + background: var(--ion-color-primary); + color: var(--ion-color-primary-contrast, #fff); + font-size: 14px; + white-space: nowrap; + cursor: pointer; +} + +.chip-title { + font-weight: 600; +} + +.chip-value { + font-weight: 400; +} + +.form-actions { + display: flex; + justify-content: flex-end; +} diff --git a/src/app/pages/search-dictation/search-dictation.page.spec.ts b/src/app/pages/search-dictation/search-dictation.page.spec.ts index d484f5a6..021f43f1 100644 --- a/src/app/pages/search-dictation/search-dictation.page.spec.ts +++ b/src/app/pages/search-dictation/search-dictation.page.spec.ts @@ -1,5 +1,8 @@ import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {TranslateService} from '@ngx-translate/core'; +import {of} from 'rxjs'; import {SearchDictationPage} from './search-dictation.page'; import {SharedTestModule} from '../../../testing/shared-test.module'; @@ -97,6 +100,7 @@ describe('SearchDictationPage', () => { })); it('showHistory only contain history which is started with input keyword', () => { + spyOn(component.dictationService, 'search'); component.history = ['apple', 'banana', 'await']; component.keyword.setValue('a'); component.filterHistory(null); @@ -104,6 +108,99 @@ describe('SearchDictationPage', () => { expect(component.filteredHistory.length).toBe(2); expect(component.filteredHistory[0]).toBe('apple'); expect(component.filteredHistory[1]).toBe('await'); + expect(component.dictationService.search).not.toHaveBeenCalled(); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelectorAll('.autocomplete ion-item').length).toBe(2); + }); + + it('uses the short keyword placeholder', () => { + const keyword = fixture.nativeElement.querySelector('ion-input[formcontrolname="keyword"]'); + expect(keyword.label).toBe('Search'); + expect(keyword.placeholder).toBe('Title, words, or ID'); + }); + + it('Enter searches only when the form is valid', fakeAsync(() => { + const search = spyOn(component.dictationService, 'search').and.returnValue(of([])); + const keywordInput = fixture.debugElement.query(By.css('ion-input[formcontrolname="keyword"]')); + + component.keyword.setValue('ab'); + fixture.detectChanges(); + keywordInput.triggerEventHandler('keydown.enter', new KeyboardEvent('keydown', {key: 'Enter'})); + tick(); + expect(search).not.toHaveBeenCalled(); + expect(component.results).toBeUndefined(); + + for (const value of ['abc', '1', '']) { + search.calls.reset(); + component.keyword.setValue(value); + fixture.detectChanges(); + keywordInput.triggerEventHandler('keydown.enter', new KeyboardEvent('keydown', {key: 'Enter'})); + tick(); + expect(search).toHaveBeenCalled(); + } + })); + + it('shows Created By without a More Options toggle', () => { + expect(fixture.nativeElement.textContent).not.toContain('More Options'); + const creator = fixture.nativeElement.querySelector('ion-input[formcontrolname="creator"]'); + const filters = fixture.nativeElement.querySelector('.filters'); + expect(creator).toBeTruthy(); + expect(creator.compareDocumentPosition(filters) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); + + it('shows the active option as a chip and chooses it from a popover', () => { + const search = spyOn(component.dictationService, 'search'); + expect(fixture.nativeElement.querySelectorAll('.filter-chip').length).toBe(3); + expect(chipText('suitable')).toBe('Suitable (Age): SuitableStudent.Any'); + expect(chipText('type')).toBe('Type: Any'); + expect(chipText('date')).toBe('Date: Any'); + expect(fixture.nativeElement.querySelector('ion-popover[trigger="suitable-filter"]')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('ion-popover[trigger="type-filter"]')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('ion-popover[trigger="date-filter"]')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('ion-select')).toBeNull(); + expect(fixture.nativeElement.querySelector('.filter-choices')).toBeNull(); + + component.chooseType('Vocab'); + fixture.detectChanges(); + expect(component.type.value).toBe('Vocab'); + expect(chipText('type')).toBe('Type: Word'); + + component.chooseSuitable('JuniorPrimary'); + fixture.detectChanges(); + expect(component.suitableStudent.value).toBe('JuniorPrimary'); + expect(chipText('suitable')).toBe('Suitable (Age): SuitableStudent.JuniorPrimary'); + + component.chooseDate(component.dateOptions[2]); + fixture.detectChanges(); + expect(component.minDate.value).toBe(component.dateOptions[2]); + expect(chipText('date')).toBe('Date: Within 3 Month'); + expect(search).not.toHaveBeenCalled(); + }); + + function chipText(group: string): string { + return fixture.nativeElement.querySelector(`[data-filter="${group}"]`).textContent.replace(/\s+/g, ' ').trim(); + } + + it('shows the 3-character message for a too-short keyword', () => { + const translate = TestBed.inject(TranslateService); + translate.setTranslation('en', { + CharactersTooShort: 'cannot less than {{length}} characters', + }); + translate.use('en'); + + component.keyword.setValue('ab'); + component.keyword.markAsDirty(); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('.error-text').textContent) + .toContain('cannot less than 3 characters'); + + component.history = ['apple']; + component.filterHistory(null); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.autocomplete')).toBeNull(); + expect(fixture.nativeElement.querySelector('.error-text').textContent) + .toContain('cannot less than 3 characters'); }); it('do not store duplicate search history', fakeAsync(() => { diff --git a/src/app/pages/search-dictation/search-dictation.page.ts b/src/app/pages/search-dictation/search-dictation.page.ts index 11a9fe1b..f1810a6c 100644 --- a/src/app/pages/search-dictation/search-dictation.page.ts +++ b/src/app/pages/search-dictation/search-dictation.page.ts @@ -1,7 +1,8 @@ -import {Component, OnInit} from '@angular/core'; +import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core'; +import {Subscription} from 'rxjs'; import {UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators} from '@angular/forms'; -import {Dictation, Dictations, SuitableStudentOptions} from '../../entity/dictation'; import {TranslateService} from '@ngx-translate/core'; +import {Dictation, Dictations, SuitableStudentOptions} from '../../entity/dictation'; import {DictationService} from '../../services/dictation/dictation.service'; import {ValidationUtils} from '../../utils/validation-utils'; import {IonicComponentService} from '../../services/ionic-component.service'; @@ -18,18 +19,26 @@ export interface DateSearchOption { styleUrls: ['./search-dictation.page.scss'], standalone: false }) -export class SearchDictationPage implements OnInit { +export class SearchDictationPage implements OnInit, OnDestroy { SEARCH_HISTORY_KEY = 'SEARCH_HISTORY_KEY'; MAX_HISTORY = 10; inputForm: UntypedFormGroup; - moreOptions = false; results: Dictation[]; suitableStudentOptions = SuitableStudentOptions; + typeOptions = [ + {value: 'Any', label: 'Any'}, + {value: 'Vocab', label: 'Word'}, + {value: 'Article', label: 'Sentence'}, + ]; dateOptions = this.createDateOptions(); history: String[] = []; filteredHistory: String[] = []; showHistory = false; + suitableChipLabel = ''; + typeChipLabel = ''; + dateChipLabel = ''; + private langChange: Subscription; constructor( public formBuilder: UntypedFormBuilder, @@ -37,10 +46,17 @@ export class SearchDictationPage implements OnInit { public translateService: TranslateService, public storage: StorageService, public ionicComponentService: IonicComponentService, + private cdr: ChangeDetectorRef, ) { } ngOnInit() { this.createForm(); + this.refreshChipLabels(); + this.langChange = this.translateService.onLangChange.subscribe(() => this.refreshChipLabels()); + } + + ngOnDestroy() { + this.langChange?.unsubscribe(); } get keyword() { return this.inputForm.get('keyword'); } @@ -67,6 +83,39 @@ export class SearchDictationPage implements OnInit { }); } + chooseSuitable(state: string) { + this.suitableStudent.setValue(state); + this.refreshChipLabels(); + } + + chooseType(value: string) { + this.type.setValue(value); + this.refreshChipLabels(); + } + + chooseDate(option: DateSearchOption) { + this.minDate.setValue(option); + this.refreshChipLabels(); + } + + private refreshChipLabels() { + this.suitableChipLabel = this.translateService.instant('SuitableStudent.' + this.suitableStudent.value); + const typeKey = this.typeOptions.find(option => option.value === this.type.value)?.label ?? ''; + this.typeChipLabel = typeKey ? this.translateService.instant(typeKey) : ''; + const dateKey = this.minDate.value?.option ?? ''; + this.dateChipLabel = dateKey ? this.translateService.instant(dateKey) : ''; + // The dev-mode double check skips this view, then fails when it re-reads the new label. + this.cdr.markForCheck(); + } + + searchOnEnter(event: Event) { + event.preventDefault(); + if (this.inputForm.invalid) { + return; + } + this.search(); + } + async search() { this.results = null; const loader = await this.ionicComponentService.showLoading(); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 8fc22d62..101bd05d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -97,5 +97,6 @@ "Preload.Tip.CreateDictation": "Create your own dictation from any word list.", "Preload.Tip.OfflineMode": "Local voice works offline — great for practice on the go.", "Preload.Tip.Images": "AI images are generated only when a dictation is saved.", - "VocabImage.UnverifiedAiNote": "AI image — may not match this word" + "VocabImage.UnverifiedAiNote": "AI image — may not match this word", + "Title, words, or ID": "Title, words, or ID" } diff --git a/src/assets/i18n/zh-Hans.json b/src/assets/i18n/zh-Hans.json index 1fecdc91..cc256d0e 100644 --- a/src/assets/i18n/zh-Hans.json +++ b/src/assets/i18n/zh-Hans.json @@ -163,7 +163,7 @@ "Dictation not found": "找不到听写练习", "Detail": "详情", "You can use the ID number to search this dictation": "您可以使用 ID 号码来搜索此练习", - "ID or part of title / description": "ID 号码或部分标题/描述", + "Title, words, or ID": "标题、生字或 ID", "Personal Information": "个人资料", "Welcome back": "欢迎回来", "Info": "资讯", diff --git a/src/assets/i18n/zh-Hant.json b/src/assets/i18n/zh-Hant.json index fb03c988..12db5e53 100644 --- a/src/assets/i18n/zh-Hant.json +++ b/src/assets/i18n/zh-Hant.json @@ -163,7 +163,7 @@ "Dictation not found": "找不到練習", "Detail": "詳情", "You can use the ID number to search this dictation": "您可以使用 ID 號碼來搜索此練習", - "ID or part of title / description": "ID 號碼或部分標題/描述", + "Title, words, or ID": "標題、生字或 ID", "Personal Information": "個人資料", "Welcome back": "歡迎回來", "Info": "資訊", From 55927714376402c358e70129919344cc46b5adce Mon Sep 17 00:00:00 2001 From: thcathy Date: Mon, 5 Oct 2026 07:20:41 +0800 Subject: [PATCH 2/4] Mark the Enter search call as an ignored promise. SonarCloud treats the unhandled search() promise as a reliability bug. Co-authored-by: Cursor --- src/app/pages/search-dictation/search-dictation.page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/search-dictation/search-dictation.page.ts b/src/app/pages/search-dictation/search-dictation.page.ts index f1810a6c..7c1c2ac6 100644 --- a/src/app/pages/search-dictation/search-dictation.page.ts +++ b/src/app/pages/search-dictation/search-dictation.page.ts @@ -113,7 +113,7 @@ export class SearchDictationPage implements OnInit, OnDestroy { if (this.inputForm.invalid) { return; } - this.search(); + void this.search(); } async search() { From f7ac69af78c3e2df0f4a86f6d0eda2d562c0513c Mon Sep 17 00:00:00 2001 From: thcathy Date: Mon, 5 Oct 2026 21:29:07 +0800 Subject: [PATCH 3/4] Keep search filter choices on a single line. Co-authored-by: Cursor --- src/app/pages/search-dictation/search-dictation.page.html | 6 +++--- src/app/pages/search-dictation/search-dictation.page.scss | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/pages/search-dictation/search-dictation.page.html b/src/app/pages/search-dictation/search-dictation.page.html index da20d481..6d34788b 100644 --- a/src/app/pages/search-dictation/search-dictation.page.html +++ b/src/app/pages/search-dictation/search-dictation.page.html @@ -61,7 +61,7 @@ @for (state of suitableStudentOptions; track state) { - {{'SuitableStudent.' + state|translate}} + {{'SuitableStudent.' + state|translate}} } @@ -70,7 +70,7 @@ @for (option of typeOptions; track option.value) { - {{option.label|translate}} + {{option.label|translate}} } @@ -79,7 +79,7 @@ @for (option of dateOptions; track option.option) { - {{option.option|translate}} + {{option.option|translate}} } diff --git a/src/app/pages/search-dictation/search-dictation.page.scss b/src/app/pages/search-dictation/search-dictation.page.scss index b5c4f1f5..67ef8866 100644 --- a/src/app/pages/search-dictation/search-dictation.page.scss +++ b/src/app/pages/search-dictation/search-dictation.page.scss @@ -71,6 +71,11 @@ ion-button { font-weight: 400; } +ion-popover { + --width: max-content; + --max-width: calc(100vw - 32px); +} + .form-actions { display: flex; justify-content: flex-end; From e28466227da4785b02fae0f971c0aa142c7ea41f Mon Sep 17 00:00:00 2001 From: thcathy Date: Mon, 5 Oct 2026 21:29:07 +0800 Subject: [PATCH 4/4] Bump version to 10.3.10 and note the search filters. Co-authored-by: Cursor --- android/app/build.gradle | 4 ++-- .../metadata/android/en-US/changelogs/default.txt | 2 +- .../metadata/android/zh-CN/changelogs/default.txt | 2 +- .../metadata/android/zh-TW/changelogs/default.txt | 2 +- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- ios/App/App/Info.plist | 4 ++-- ios/App/fastlane/metadata/en-US/release_notes.txt | 2 +- ios/App/fastlane/metadata/zh-Hans/release_notes.txt | 2 +- ios/App/fastlane/metadata/zh-Hant/release_notes.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- release_notes/en.txt | 2 +- release_notes/zh-Hans.txt | 2 +- release_notes/zh-Hant.txt | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b14e8287..3ba2befa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.esl.ionic" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1003009 - versionName "10.3.9" + versionCode 1003010 + versionName "10.3.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/fastlane/metadata/android/en-US/changelogs/default.txt b/android/fastlane/metadata/android/en-US/changelogs/default.txt index acc61753..914a1191 100644 --- a/android/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/android/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1 +1 @@ -Each dictation in a list now shows a short preview under the title, so you can pick the right practice without opening it. +Search by title, words, or ID, then press Enter. Age, type, and date are chips you can tap to change, and each list row now shows its preview at the bottom. diff --git a/android/fastlane/metadata/android/zh-CN/changelogs/default.txt b/android/fastlane/metadata/android/zh-CN/changelogs/default.txt index 1520e378..67a39629 100644 --- a/android/fastlane/metadata/android/zh-CN/changelogs/default.txt +++ b/android/fastlane/metadata/android/zh-CN/changelogs/default.txt @@ -1 +1 @@ -列表里每份练习的标题下会显示一行预览,方便你不用打开就能选对听写练习。 +可以用标题、生字或 ID 寻找练习,按回车开始。年龄、类型和日期点一下就能改,列表里每份练习的预览改到最下方。 diff --git a/android/fastlane/metadata/android/zh-TW/changelogs/default.txt b/android/fastlane/metadata/android/zh-TW/changelogs/default.txt index 7989093d..22cba292 100644 --- a/android/fastlane/metadata/android/zh-TW/changelogs/default.txt +++ b/android/fastlane/metadata/android/zh-TW/changelogs/default.txt @@ -1 +1 @@ -列表裡每份練習的標題下會顯示一行預覽,方便你不用打開就能選對默書。 +可以用標題、生字或 ID 尋找練習,按 Enter 開始。年齡、類型和日期點一下就能改,列表裡每份練習的預覽改到最下方。 diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index c173ae8f..7d6a5446 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -354,12 +354,12 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1003009; + CURRENT_PROJECT_VERSION = 1003010; DEVELOPMENT_TEAM = DU483K8F48; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 10.3.9; + MARKETING_VERSION = 10.3.10; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = com.esl.ionic; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -378,12 +378,12 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1003009; + CURRENT_PROJECT_VERSION = 1003010; DEVELOPMENT_TEAM = DU483K8F48; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 10.3.9; + MARKETING_VERSION = 10.3.10; PRODUCT_BUNDLE_IDENTIFIER = com.esl.ionic; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index ee92329a..049686b3 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 10.3.9 + 10.3.10 CFBundleURLTypes @@ -34,7 +34,7 @@ CFBundleVersion - 1003009 + 1003010 LSApplicationCategoryType LSRequiresIPhoneOS diff --git a/ios/App/fastlane/metadata/en-US/release_notes.txt b/ios/App/fastlane/metadata/en-US/release_notes.txt index acc61753..914a1191 100644 --- a/ios/App/fastlane/metadata/en-US/release_notes.txt +++ b/ios/App/fastlane/metadata/en-US/release_notes.txt @@ -1 +1 @@ -Each dictation in a list now shows a short preview under the title, so you can pick the right practice without opening it. +Search by title, words, or ID, then press Enter. Age, type, and date are chips you can tap to change, and each list row now shows its preview at the bottom. diff --git a/ios/App/fastlane/metadata/zh-Hans/release_notes.txt b/ios/App/fastlane/metadata/zh-Hans/release_notes.txt index 1520e378..67a39629 100644 --- a/ios/App/fastlane/metadata/zh-Hans/release_notes.txt +++ b/ios/App/fastlane/metadata/zh-Hans/release_notes.txt @@ -1 +1 @@ -列表里每份练习的标题下会显示一行预览,方便你不用打开就能选对听写练习。 +可以用标题、生字或 ID 寻找练习,按回车开始。年龄、类型和日期点一下就能改,列表里每份练习的预览改到最下方。 diff --git a/ios/App/fastlane/metadata/zh-Hant/release_notes.txt b/ios/App/fastlane/metadata/zh-Hant/release_notes.txt index 7989093d..22cba292 100644 --- a/ios/App/fastlane/metadata/zh-Hant/release_notes.txt +++ b/ios/App/fastlane/metadata/zh-Hant/release_notes.txt @@ -1 +1 @@ -列表裡每份練習的標題下會顯示一行預覽,方便你不用打開就能選對默書。 +可以用標題、生字或 ID 尋找練習,按 Enter 開始。年齡、類型和日期點一下就能改,列表裡每份練習的預覽改到最下方。 diff --git a/package-lock.json b/package-lock.json index 9087c9a4..b7363188 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "esl-ionic", - "version": "10.3.9", + "version": "10.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "esl-ionic", - "version": "10.3.9", + "version": "10.3.10", "dependencies": { "@angular/animations": "^21.1.3", "@angular/common": "^21.1.3", diff --git a/package.json b/package.json index 03edacf9..6093b32f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "esl-ionic", - "version": "10.3.9", + "version": "10.3.10", "author": "thcathy", "homepage": "https://www.funfunspell.com", "scripts": { diff --git a/release_notes/en.txt b/release_notes/en.txt index acc61753..914a1191 100644 --- a/release_notes/en.txt +++ b/release_notes/en.txt @@ -1 +1 @@ -Each dictation in a list now shows a short preview under the title, so you can pick the right practice without opening it. +Search by title, words, or ID, then press Enter. Age, type, and date are chips you can tap to change, and each list row now shows its preview at the bottom. diff --git a/release_notes/zh-Hans.txt b/release_notes/zh-Hans.txt index 1520e378..67a39629 100644 --- a/release_notes/zh-Hans.txt +++ b/release_notes/zh-Hans.txt @@ -1 +1 @@ -列表里每份练习的标题下会显示一行预览,方便你不用打开就能选对听写练习。 +可以用标题、生字或 ID 寻找练习,按回车开始。年龄、类型和日期点一下就能改,列表里每份练习的预览改到最下方。 diff --git a/release_notes/zh-Hant.txt b/release_notes/zh-Hant.txt index 7989093d..22cba292 100644 --- a/release_notes/zh-Hant.txt +++ b/release_notes/zh-Hant.txt @@ -1 +1 @@ -列表裡每份練習的標題下會顯示一行預覽,方便你不用打開就能選對默書。 +可以用標題、生字或 ID 尋找練習,按 Enter 開始。年齡、類型和日期點一下就能改,列表裡每份練習的預覽改到最下方。