Skip to content

Commit 19ff947

Browse files
committed
release: v1.0.1 播放页设置抽屉与共用设置 Fragment
- 菜单键打开右侧抽屉,左侧子菜单(地址管理、切换源、EPG、播放选项) - 帮助弹窗单独一行展示版本号;切换源子菜单仅显示线路编号 - 更新 CHANGELOG Made-with: Cursor
1 parent f5b785f commit 19ff947

16 files changed

Lines changed: 1370 additions & 329 deletions

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@
2121

2222
- 新增 7 个 PlayerManager 单元测试,覆盖频道切换状态隔离、陈旧回调防护、全部失败后恢复等场景
2323

24+
## [1.0.1] - 2026-03-21
25+
26+
### Added
27+
28+
- 播放页菜单键 / F6:右侧设置抽屉(遮罩 + 面板),不离开播放界面
29+
- 设置主菜单在右、子菜单在左:地址管理、切换源(仅播放页)、EPG、播放选项;帮助打开独立弹窗
30+
- `SettingsCollapsibleFragment``SettingsPanelHost`,播放页与独立设置页共用同一套设置 UI
31+
- 帮助弹窗内单独一行展示应用版本号(`versionName`
32+
33+
### Changed
34+
35+
- `SettingsActivity` 改为全屏承载设置 Fragment;原左右分栏布局移除
36+
- 切换源子菜单仅显示「线路 x」,不展示播放 URL
37+
- 设置抽屉总宽度约 480dp,便于双栏主/子菜单
38+
2439
## [1.0.0] - 2026-03-20
2540

2641
### Added
@@ -44,5 +59,6 @@
4459
- 自动播放在 Activity 重建时重复触发
4560
- 刷新播放源时频道 ID 重建导致收藏记录级联删除
4661

47-
[Unreleased]: https://github.com/whyun-android/witv/compare/v1.0.0...HEAD
62+
[Unreleased]: https://github.com/whyun-android/witv/compare/v1.0.1...HEAD
63+
[1.0.1]: https://github.com/whyun-android/witv/compare/v1.0.0...v1.0.1
4864
[1.0.0]: https://github.com/whyun-android/witv/releases/tag/v1.0.0

app/src/main/java/com/whyun/witv/ui/PlayerActivity.java

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import java.util.concurrent.ExecutorService;
3939
import java.util.concurrent.Executors;
4040

41-
public class PlayerActivity extends FragmentActivity implements PlayerManager.Callback {
41+
public class PlayerActivity extends FragmentActivity implements PlayerManager.Callback, SettingsPanelHost {
4242

4343
public static final String EXTRA_CHANNEL_ID = "channel_id";
4444
public static final String EXTRA_SOURCE_ID = "source_id";
@@ -60,6 +60,8 @@ public class PlayerActivity extends FragmentActivity implements PlayerManager.Ca
6060
private TextView switchingToast;
6161
private TextView loadSpeedOverlay;
6262
private RecyclerView channelListOverlay;
63+
private View settingsPanelOverlay;
64+
private PlayerView playerView;
6365

6466
private boolean isFavorite = false;
6567

@@ -126,6 +128,11 @@ protected void onCreate(Bundle savedInstanceState) {
126128

127129
initViews();
128130
initPlayer();
131+
if (savedInstanceState == null) {
132+
getSupportFragmentManager().beginTransaction()
133+
.replace(R.id.settings_panel_content, new SettingsCollapsibleFragment(), "settings_drawer")
134+
.commitNow();
135+
}
129136
applyLoadSpeedOverlayPreference();
130137
loadAndPlay();
131138
}
@@ -144,10 +151,13 @@ private void initViews() {
144151
loadSpeedOverlay = findViewById(R.id.load_speed_overlay);
145152
channelListOverlay = findViewById(R.id.channel_list_overlay);
146153
channelListOverlay.setLayoutManager(new LinearLayoutManager(this));
154+
155+
settingsPanelOverlay = findViewById(R.id.settings_panel_overlay);
156+
findViewById(R.id.settings_scrim).setOnClickListener(v -> hideSettingsPanel());
147157
}
148158

149159
private void initPlayer() {
150-
PlayerView playerView = findViewById(R.id.player_view);
160+
playerView = findViewById(R.id.player_view);
151161
playerView.setUseController(false);
152162

153163
playerManager = new PlayerManager(this);
@@ -398,6 +408,64 @@ private void hideOverlay() {
398408
overlayVisible = false;
399409
}
400410

411+
private void hideSettingsPanel() {
412+
SettingsCollapsibleFragment f = (SettingsCollapsibleFragment) getSupportFragmentManager()
413+
.findFragmentByTag("settings_drawer");
414+
if (f != null) {
415+
f.onSettingsDrawerDismiss();
416+
}
417+
if (settingsPanelOverlay != null) {
418+
settingsPanelOverlay.setVisibility(View.GONE);
419+
}
420+
if (playerView != null) {
421+
playerView.requestFocus();
422+
}
423+
}
424+
425+
private void showSettingsPanel() {
426+
if (settingsPanelOverlay == null) {
427+
return;
428+
}
429+
settingsPanelOverlay.setVisibility(View.VISIBLE);
430+
SettingsCollapsibleFragment f = (SettingsCollapsibleFragment) getSupportFragmentManager()
431+
.findFragmentByTag("settings_drawer");
432+
if (f != null) {
433+
f.refreshAndFocus();
434+
}
435+
}
436+
437+
private boolean isSettingsPanelVisible() {
438+
return settingsPanelOverlay != null && settingsPanelOverlay.getVisibility() == View.VISIBLE;
439+
}
440+
441+
/** 供 {@link SettingsCollapsibleFragment} 读取当前频道以展示「切换源」列表 */
442+
public long getCurrentChannelIdForPanel() {
443+
return currentChannelId;
444+
}
445+
446+
@Override
447+
public boolean shouldShowStreamSwitchGroup() {
448+
return currentChannelId > 0;
449+
}
450+
451+
@Override
452+
public PlayerManager getPlayerManagerOrNull() {
453+
return playerManager;
454+
}
455+
456+
@Override
457+
public void onManualStreamSwitch(int index) {
458+
if (playerManager != null) {
459+
playerManager.manualSwitchSource(index);
460+
}
461+
}
462+
463+
@Override
464+
public void onPlaybackOverlayPreferenceChanged() {
465+
applyLoadSpeedOverlayPreference();
466+
startLoadSpeedRefreshIfNeeded();
467+
}
468+
401469
private void switchChannel(int direction) {
402470
if (allChannels == null || allChannels.isEmpty()) return;
403471

@@ -438,6 +506,22 @@ private void showChannelList() {
438506

439507
@Override
440508
public boolean onKeyDown(int keyCode, KeyEvent event) {
509+
if (isSettingsPanelVisible()) {
510+
if (keyCode == KeyEvent.KEYCODE_BACK) {
511+
SettingsCollapsibleFragment f = (SettingsCollapsibleFragment) getSupportFragmentManager()
512+
.findFragmentByTag("settings_drawer");
513+
if (f != null && f.handleBack()) {
514+
return true;
515+
}
516+
hideSettingsPanel();
517+
return true;
518+
}
519+
if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_F6) {
520+
hideSettingsPanel();
521+
return true;
522+
}
523+
}
524+
441525
switch (keyCode) {
442526
case KeyEvent.KEYCODE_DPAD_UP:
443527
if (channelListOverlay.getVisibility() != View.VISIBLE) {
@@ -492,7 +576,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) {
492576
return true;
493577
case KeyEvent.KEYCODE_MENU:
494578
case KeyEvent.KEYCODE_F6:
495-
startActivity(new android.content.Intent(this, SettingsActivity.class));
579+
showSettingsPanel();
496580
return true;
497581
case KeyEvent.KEYCODE_BOOKMARK:
498582
case KeyEvent.KEYCODE_STAR:
Lines changed: 16 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,40 @@
11
package com.whyun.witv.ui;
22

3-
import android.net.wifi.WifiInfo;
4-
import android.net.wifi.WifiManager;
53
import android.os.Bundle;
6-
import android.widget.Button;
7-
import android.widget.CheckBox;
8-
import android.widget.EditText;
9-
import android.widget.TextView;
10-
import android.widget.Toast;
114

125
import androidx.fragment.app.FragmentActivity;
13-
import androidx.recyclerview.widget.LinearLayoutManager;
14-
import androidx.recyclerview.widget.RecyclerView;
156

167
import com.whyun.witv.R;
17-
import com.whyun.witv.data.PreferenceManager;
18-
import com.whyun.witv.data.db.AppDatabase;
19-
import com.whyun.witv.data.db.entity.M3USource;
20-
import com.whyun.witv.data.repository.ChannelRepository;
21-
import com.whyun.witv.data.repository.EpgRepository;
8+
import com.whyun.witv.player.PlayerManager;
229

23-
import java.util.ArrayList;
24-
import java.util.List;
25-
import java.util.Locale;
26-
import java.util.concurrent.ExecutorService;
27-
import java.util.concurrent.Executors;
28-
29-
public class SettingsActivity extends FragmentActivity {
30-
31-
private AppDatabase db;
32-
private ChannelRepository channelRepo;
33-
private EpgRepository epgRepo;
34-
private PreferenceManager preferenceManager;
35-
private SourceListAdapter adapter;
36-
private EditText epgUrlInput;
37-
private final ExecutorService executor = Executors.newSingleThreadExecutor();
10+
public class SettingsActivity extends FragmentActivity implements SettingsPanelHost {
3811

3912
@Override
4013
protected void onCreate(Bundle savedInstanceState) {
4114
super.onCreate(savedInstanceState);
4215
setContentView(R.layout.activity_settings);
43-
44-
db = AppDatabase.getInstance(this);
45-
channelRepo = new ChannelRepository(this);
46-
epgRepo = new EpgRepository(this);
47-
preferenceManager = new PreferenceManager(this);
48-
49-
setupWebHint();
50-
setupSourceList();
51-
setupSettings();
52-
setupAutoPlay();
53-
setupShowLoadSpeed();
54-
}
55-
56-
private void setupWebHint() {
57-
TextView webHint = findViewById(R.id.web_hint);
58-
String ip = getDeviceIp();
59-
webHint.setText(String.format("通过浏览器管理:http://%s:9978", ip));
60-
}
61-
62-
private void setupSourceList() {
63-
RecyclerView sourceList = findViewById(R.id.source_list);
64-
sourceList.setLayoutManager(new LinearLayoutManager(this));
65-
adapter = new SourceListAdapter(new ArrayList<>(), this::onActivateSource);
66-
sourceList.setAdapter(adapter);
67-
loadSources();
68-
}
69-
70-
private void setupSettings() {
71-
epgUrlInput = findViewById(R.id.epg_url_input);
72-
Button saveEpg = findViewById(R.id.btn_save_epg);
73-
Button reloadEpg = findViewById(R.id.btn_reload_epg);
74-
75-
executor.execute(() -> {
76-
M3USource active = db.m3uSourceDao().getActive();
77-
if (active != null && active.epgUrl != null) {
78-
runOnUiThread(() -> epgUrlInput.setText(active.epgUrl));
79-
}
80-
});
81-
82-
saveEpg.setOnClickListener(v -> {
83-
String epgUrl = epgUrlInput.getText().toString().trim();
84-
executor.execute(() -> {
85-
M3USource active = db.m3uSourceDao().getActive();
86-
if (active != null) {
87-
active.epgUrl = epgUrl;
88-
db.m3uSourceDao().update(active);
89-
runOnUiThread(() ->
90-
Toast.makeText(this, "EPG 设置已保存", Toast.LENGTH_SHORT).show());
91-
}
92-
});
93-
});
94-
95-
reloadEpg.setOnClickListener(v -> {
96-
executor.execute(() -> {
97-
M3USource active = db.m3uSourceDao().getActive();
98-
if (active != null && active.epgUrl != null && !active.epgUrl.isEmpty()) {
99-
runOnUiThread(() ->
100-
Toast.makeText(this, "正在刷新 EPG…", Toast.LENGTH_SHORT).show());
101-
try {
102-
epgRepo.loadEpg(active.epgUrl);
103-
preferenceManager.markEpgAutoRefreshSuccess(active.epgUrl);
104-
runOnUiThread(() ->
105-
Toast.makeText(this, "EPG 刷新完成", Toast.LENGTH_SHORT).show());
106-
} catch (Exception e) {
107-
runOnUiThread(() ->
108-
Toast.makeText(this, "EPG 刷新失败: " + e.getMessage(),
109-
Toast.LENGTH_LONG).show());
110-
}
111-
} else {
112-
runOnUiThread(() ->
113-
Toast.makeText(this, "请先设置 EPG 地址", Toast.LENGTH_SHORT).show());
114-
}
115-
});
116-
});
117-
}
118-
119-
private void setupAutoPlay() {
120-
CheckBox autoPlayCheck = findViewById(R.id.cb_auto_play_last);
121-
autoPlayCheck.setChecked(preferenceManager.isAutoPlayLastEnabled());
122-
autoPlayCheck.setOnCheckedChangeListener((buttonView, isChecked) -> {
123-
preferenceManager.setAutoPlayLast(isChecked);
124-
Toast.makeText(this,
125-
isChecked ? "已开启启动播放上次频道" : "已关闭启动播放上次频道",
126-
Toast.LENGTH_SHORT).show();
127-
});
128-
}
129-
130-
private void setupShowLoadSpeed() {
131-
CheckBox cb = findViewById(R.id.cb_show_load_speed);
132-
cb.setChecked(preferenceManager.isShowLoadSpeedOverlay());
133-
cb.setOnCheckedChangeListener((buttonView, isChecked) -> {
134-
preferenceManager.setShowLoadSpeedOverlay(isChecked);
135-
Toast.makeText(this,
136-
isChecked ? "已开启播放页加载速度显示" : "已关闭播放页加载速度显示",
137-
Toast.LENGTH_SHORT).show();
138-
});
16+
if (savedInstanceState == null) {
17+
getSupportFragmentManager().beginTransaction()
18+
.replace(R.id.settings_fragment_container, new SettingsCollapsibleFragment())
19+
.commit();
20+
}
13921
}
14022

141-
private void loadSources() {
142-
executor.execute(() -> {
143-
List<M3USource> sources = db.m3uSourceDao().getAll();
144-
runOnUiThread(() -> adapter.updateData(sources));
145-
});
23+
@Override
24+
public boolean shouldShowStreamSwitchGroup() {
25+
return false;
14626
}
14727

148-
private void onActivateSource(M3USource source) {
149-
executor.execute(() -> {
150-
db.m3uSourceDao().deactivateAll();
151-
db.m3uSourceDao().activate(source.id);
152-
153-
try {
154-
List<com.whyun.witv.data.db.entity.Channel> channels =
155-
db.channelDao().getBySource(source.id);
156-
if (channels.isEmpty()) {
157-
channelRepo.loadSource(source);
158-
}
159-
} catch (Exception e) {
160-
e.printStackTrace();
161-
}
162-
163-
loadSources();
164-
runOnUiThread(() ->
165-
Toast.makeText(this, "已切换到: " + source.name, Toast.LENGTH_SHORT).show());
166-
});
28+
@Override
29+
public PlayerManager getPlayerManagerOrNull() {
30+
return null;
16731
}
16832

169-
private String getDeviceIp() {
170-
try {
171-
WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
172-
if (wifiManager != null) {
173-
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
174-
int ipInt = wifiInfo.getIpAddress();
175-
if (ipInt != 0) {
176-
return String.format(Locale.US, "%d.%d.%d.%d",
177-
(ipInt & 0xff), (ipInt >> 8 & 0xff),
178-
(ipInt >> 16 & 0xff), (ipInt >> 24 & 0xff));
179-
}
180-
}
181-
} catch (Exception ignored) {
182-
}
183-
return "0.0.0.0";
33+
@Override
34+
public void onManualStreamSwitch(int index) {
18435
}
18536

18637
@Override
187-
protected void onDestroy() {
188-
super.onDestroy();
189-
executor.shutdown();
38+
public void onPlaybackOverlayPreferenceChanged() {
19039
}
19140
}

0 commit comments

Comments
 (0)