Skip to content
Merged

Dev #64

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
caaada0
fix(tv): switch categories at standalone vod edges
Silent1566 Sep 7, 2026
440a301
fix(tv): restore content focus after category edge switch in standalo…
Silent1566 Sep 7, 2026
bf140d1
fix(tv): preserve all content rows across category edge switch
Silent1566 Sep 8, 2026
4429621
fix(home): restore category button focus with default VOD loading
Silent1566 Sep 8, 2026
1c0bd8e
fix(tv): repair episode detail photo focus navigation
Silent1566 Sep 8, 2026
045ae26
docs: design audio ad rules v2 upgrade
Silent1566 Sep 8, 2026
fa8f895
feat(ad): add bounded speech ad rule parser and matcher
Silent1566 Sep 8, 2026
13a12ee
fix(home): delay category button focus until after fragment transacti…
Silent1566 Sep 8, 2026
411171e
docs(ad): specify phase 2 speech runtime integration gate
Silent1566 Sep 8, 2026
400c059
fix(tv): restore colorful detail episode focus after dismiss
Silent1566 Sep 8, 2026
f89c301
fix(tv): restore long-pressed colorful episode focus
Silent1566 Sep 8, 2026
4d42cc3
merge: 同步 beta 并完成 dev3 TV 焦点复评
Silent1566 Sep 8, 2026
de4e1f5
修复 WebHome fm.vod 打开有历史的影片不续播(多季 TMDB 剧回落第 1 集):playVod 按组合键 siteKey@…
power721 Sep 8, 2026
1e56428
fix(ad): isolate speech recognition execution and lifecycle
Silent1566 Sep 8, 2026
e8d82db
Merge pull request #240 from power721/fix/webhome-vod-resume
Silent1566 Sep 8, 2026
db8e984
feat(ad): connect compound speech ad rules
Silent1566 Sep 8, 2026
7c03957
fix(tv): repair episode detail photo focus navigation
Silent1566 Sep 8, 2026
853822b
fix(tv): restore colorful detail episode focus after dismiss
Silent1566 Sep 8, 2026
5242aa1
fix(tv): restore long-pressed colorful episode focus
Silent1566 Sep 8, 2026
62fd7f1
fix(webhome): backport fm.vod history resume to beta (#240)
power721 Sep 8, 2026
242c089
docs: retain PR 239 review evidence after PR 238 split
Silent1566 Sep 8, 2026
da8d4d2
docs(ad): record phase 3 rule settings design
Silent1566 Sep 8, 2026
f5096d4
fix(home): reveal category header after edge switch
Silent1566 Sep 8, 2026
66a0a82
Implement speech ad rules V2 settings and backup
Silent1566 Sep 9, 2026
6fd2687
merge: 合并 beta 最新代码并完成 dev4 复评
Silent1566 Sep 9, 2026
41133f8
chore: 移除误提交的本地备份文件
Silent1566 Sep 9, 2026
515d1e0
docs: 收口 dev4 beta 合并评审记录
Silent1566 Sep 9, 2026
a65c732
Merge pull request #242 from Silent1566/dev4
Silent1566 Sep 9, 2026
9769ca8
Keep live playback autoplay independent from VOD setting
Silent1566 Sep 9, 2026
c3397ba
feat(ad-audio): add playback health gate to suppress speech on degrad…
Silent1566 Sep 9, 2026
41fee8b
dev4: 修正自动播放回归测试并记录 beta 合并评审 (2026-09-09 round4)
Silent1566 Sep 9, 2026
fbb7197
feat(ad-audio): add speech ad rules V2 (P4) and merge beta latest (20…
Silent1566 Sep 9, 2026
15ca94a
Merge pull request #244 from Silent1566/dev2
Silent1566 Sep 9, 2026
5246948
Merge pull request #243 from Silent1566/dev4
Silent1566 Sep 9, 2026
8c0e53f
Merge remote-tracking branch 'bd/beta' into dev
YaChengMu Sep 9, 2026
0180f8e
Merge remote-tracking branch 'bd/dev3' into dev
YaChengMu Sep 9, 2026
e415870
Merge remote-tracking branch 'bd/main' into dev
YaChengMu Sep 9, 2026
7f9adba
refactor(ad): 简化广告反馈为AI检测流程并移除主题动态配置
YaChengMu Sep 9, 2026
4c29af7
refactor(mobile): 公开 file 方法以便外部调用
YaChengMu Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,7 @@ public void onCategoryContentHorizontalEdge(Class item, int contentRow, boolean
mBinding.typeRecycler.removeCallbacks(mTypeSwitch);
int position = mTypeAdapter.indexOf(item);
mBinding.typeRecycler.setSelectedPosition(position);
if (contentRow == 0) focusFirstCard(item);
else focusCategoryButton(item);
focusCategoryButton(item);
}

private Class getAdjacentCategory(Class item, boolean towardEnd) {
Expand All @@ -288,10 +287,23 @@ private void focusFirstCard(Class item) {
}

private void focusCategoryButton(Class item) {
mBinding.typeRecycler.setVisibility(View.VISIBLE);
updateToolbarVisibility(true);
mBinding.typeRecycler.requestFocus();
showCategoryContent(item);
// Complete the switch before checking the new page; otherwise the posted callback can
// run while the target fragment is not added yet and drop the header/focus restoration.
getSupportFragmentManager().executePendingTransactions();
mBinding.typeRecycler.post(() -> {
if (isFinishing() || isDestroyed() || !isCurrentCategory(item)) return;
int position = mTypeAdapter.indexOf(item);
if (position < 0 || mBinding.typeRecycler.getSelectedPosition() != position) return;
mBinding.typeRecycler.setVisibility(View.VISIBLE);
updateToolbarVisibility(true);
mFolder.scrollContentToTop();
mBinding.typeRecycler.setSelectedPosition(position, holder -> {
if (isCurrentCategory(item) && mBinding.typeRecycler.getSelectedPosition() == position) {
holder.itemView.requestFocus();
}
});
});
}

private void showHomeContent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public class LiveActivity extends PlaybackActivity implements GroupAdapter.OnCli

private static final long PLAYBACK_END_RETRY_DELAY = 500;

@Override
protected boolean shouldAutoPlay() {
return true;
}

private ActivityLiveBinding mBinding;
private ChannelAdapter mChannelAdapter;
private EpgDataAdapter mEpgDataAdapter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public class SettingAdActivity extends BaseActivity {
private volatile int probeRuleCount;
private final ActivityResultLauncher<String[]> adAudioRulePicker =
registerForActivityResult(new ActivityResultContracts.OpenDocument(), this::importAdAudioRules);
private final ActivityResultLauncher<String[]> speechAdRulePicker =
registerForActivityResult(new ActivityResultContracts.OpenDocument(), this::importSpeechAdRules);

public static void start(Activity activity) {
activity.startActivity(new Intent(activity, SettingAdActivity.class));
Expand Down Expand Up @@ -89,6 +91,8 @@ protected void initEvent() {
mBinding.probeRuleRefresh.setOnClickListener(this::refreshProbeRules);
mBinding.speechAdEnabled.setOnClickListener(this::toggleSpeechAdEnabled);
mBinding.speechAdKeywords.setOnClickListener(this::editSpeechAdKeywords);
mBinding.speechAdRules.setOnClickListener(this::manageSpeechAdRules);
mBinding.speechAdBuiltin.setOnClickListener(this::toggleSpeechAdBuiltin);
mBinding.speechAdSkipSeconds.setOnClickListener(this::editSpeechAdSkipSeconds);
mBinding.speechAdSkipMode.setOnClickListener(this::selectSpeechAdSkipMode);
mBinding.autoSkipIntroOutro.setOnClickListener(this::setAutoSkipIntroOutro);
Expand Down Expand Up @@ -126,8 +130,11 @@ private void setText() {
safeSet("probeRuleSource", mBinding.probeRuleSourceText, this::getProbeRuleSourceText);
safeSet("probeRuleRefresh", mBinding.probeRuleRefreshText, this::getProbeRuleRefreshText);
SpeechAdConfig speech = SpeechAdSetting.snapshot();
SpeechAdSetting.RuleSnapshot speechRules = SpeechAdSetting.ruleSnapshot();
safeSet("speechAdEnabled", mBinding.speechAdEnabledText, () -> getSpeechAdEnabledText(speech));
safeSet("speechAdKeywords", mBinding.speechAdKeywordsText, () -> getString(R.string.speech_ad_keyword_count, speech.keywords().values().size()));
safeSet("speechAdRules", mBinding.speechAdRulesText, () -> getSpeechAdRulesText(speechRules));
safeSet("speechAdBuiltin", mBinding.speechAdBuiltinText, () -> getSpeechAdBuiltinText(speechRules));
safeSet("speechAdSkipSeconds", mBinding.speechAdSkipSecondsText, () -> getString(R.string.speech_ad_skip_seconds_value, speech.skipSeconds()));
safeSet("speechAdSkipMode", mBinding.speechAdSkipModeText, () -> speech.mode() == AdSkipPolicyController.Mode.AUTO
? getString(R.string.speech_ad_skip_mode_auto) : getString(R.string.speech_ad_skip_mode_prompt));
Expand Down Expand Up @@ -347,6 +354,145 @@ private void toggleSpeechAdEnabled(View view) {
setText();
}

private String getSpeechAdRulesText(SpeechAdSetting.RuleSnapshot snapshot) {
if (snapshot.hasError()) return getString(R.string.speech_ad_rules_error, snapshot.error());
if (snapshot.rules().isEmpty()) return getString(R.string.speech_ad_rules_none);
return getString(R.string.speech_ad_rules_summary,
snapshot.rules().rules().size(), getSpeechAdRuleSourceText(snapshot));
}

private String getSpeechAdBuiltinText(SpeechAdSetting.RuleSnapshot snapshot) {
int count = Math.max(snapshot.builtinRules().rules().size(), SpeechAdSetting.builtinRuleCount());
return getString(R.string.speech_ad_builtin_value,
getSwitch(snapshot.builtinEnabled()), count);
}

private String getSpeechAdRuleSourceText(SpeechAdSetting.RuleSnapshot snapshot) {
boolean hasCustom = !snapshot.customRules().isEmpty();
boolean hasBuiltin = snapshot.builtinEnabled() && !snapshot.builtinRules().isEmpty();
if (hasCustom && hasBuiltin) return getString(R.string.speech_ad_rules_source_combined);
if (hasBuiltin) return getString(R.string.speech_ad_rules_source_builtin);
if (snapshot.source() == SpeechAdSetting.RuleSource.IMPORTED) {
return getString(R.string.speech_ad_rules_source_imported);
}
if (snapshot.source() == SpeechAdSetting.RuleSource.USER) {
return getString(R.string.speech_ad_rules_source_user);
}
return getString(R.string.speech_ad_rules_source_none);
}

private void manageSpeechAdRules(View view) {
new MaterialAlertDialogBuilder(this, R.style.Theme_WebHTV_LightDialog)
.setTitle(R.string.speech_ad_rules_manage)
.setItems(new String[]{
getString(R.string.speech_ad_rules_edit),
getString(R.string.speech_ad_rules_import),
getString(R.string.speech_ad_rules_clear),
getString(R.string.speech_ad_rules_view_builtin)
}, (dialog, which) -> {
if (which == 0) editSpeechAdRules();
else if (which == 1) speechAdRulePicker.launch(new String[]{"text/plain", "text/*"});
else if (which == 2) confirmClearSpeechAdRules();
else showBuiltinSpeechAdRules();
})
.setNegativeButton(R.string.dialog_negative, null)
.show();
}

private void showBuiltinSpeechAdRules() {
try {
new MaterialAlertDialogBuilder(this, R.style.Theme_WebHTV_LightDialog)
.setTitle(R.string.speech_ad_rules_view_builtin)
.setMessage(SpeechAdSetting.builtinRulesText())
.setPositiveButton(R.string.dialog_positive, null)
.show();
} catch (RuntimeException error) {
Notify.show(getString(R.string.speech_ad_rules_error, SpeechAdSetting.safeError(error)));
}
}

private void editSpeechAdRules() {
EditText input = new EditText(this);
input.setGravity(Gravity.TOP | Gravity.START);
input.setMinLines(8);
input.setMaxLines(16);
input.setSingleLine(false);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
input.setText(SpeechAdSetting.customRulesText());
input.setSelection(input.length());
AlertDialog dialog = new MaterialAlertDialogBuilder(this, R.style.Theme_WebHTV_LightDialog)
.setTitle(R.string.speech_ad_rules_edit)
.setView(input)
.setNegativeButton(R.string.dialog_negative, null)
.setPositiveButton(R.string.dialog_positive, null)
.create();
dialog.setOnShowListener(ignored -> dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(button -> {
try {
String text = input.getText().toString();
if (text.trim().isEmpty()) SpeechAdSetting.clearRules();
else SpeechAdSetting.setRulesText(text, SpeechAdSetting.RuleSource.USER);
Notify.show(R.string.speech_ad_rules_saved);
notifyAdAudioRuntime();
setText();
dialog.dismiss();
} catch (RuntimeException error) {
input.setError(SpeechAdSetting.safeError(error));
input.requestFocus();
}
}));
dialog.show();
LightDialog.apply(dialog);
}

private void importSpeechAdRules(Uri uri) {
if (uri == null) return;
Task.execute(() -> {
boolean imported = false;
String message;
try {
SpeechAdSetting.importUri(getContentResolver(), uri);
imported = true;
message = getString(R.string.speech_ad_rules_imported,
SpeechAdSetting.ruleSnapshot().customRules().rules().size());
} catch (Exception error) {
message = getString(R.string.speech_ad_rules_import_failed,
SpeechAdSetting.safeError(error));
}
final boolean success = imported;
String result = message;
runOnUiThread(() -> {
if (success) notifyAdAudioRuntime();
if (!canSetText()) return;
Notify.show(result);
setText();
});
});
}

private void confirmClearSpeechAdRules() {
new MaterialAlertDialogBuilder(this, R.style.Theme_WebHTV_LightDialog)
.setTitle(R.string.speech_ad_rules_clear)
.setMessage(R.string.speech_ad_rules_clear_confirm)
.setPositiveButton(R.string.dialog_positive, (dialog, which) -> {
SpeechAdSetting.clearRules();
Notify.show(R.string.speech_ad_rules_clear_done);
notifyAdAudioRuntime();
setText();
})
.setNegativeButton(R.string.dialog_negative, null)
.show();
}

private void toggleSpeechAdBuiltin(View view) {
try {
SpeechAdSetting.setBuiltinEnabled(!SpeechAdSetting.isBuiltinEnabled());
notifyAdAudioRuntime();
} catch (RuntimeException error) {
Notify.show(getString(R.string.speech_ad_rules_error, SpeechAdSetting.safeError(error)));
}
setText();
}

private void editSpeechAdKeywords(View view) {
SpeechAdConfig speech = SpeechAdSetting.snapshot();
EditText input = new EditText(this);
Expand Down
Loading
Loading