index.html 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. {% extends "base.html" %}
  2. {% block title %}电视控制{% endblock %}
  3. {% block extra_styles %}
  4. .tv-grid {
  5. display: flex;
  6. flex-wrap: wrap;
  7. gap: 15px;
  8. margin-bottom: 20px;
  9. justify-content: flex-start;
  10. }
  11. .tv-card {
  12. flex: 0 0 auto;
  13. width: 140px;
  14. background: #f8f9fa;
  15. border-radius: 12px;
  16. padding: 15px;
  17. text-align: center;
  18. border: 1px solid #eee;
  19. transition: all 0.2s;
  20. cursor: pointer;
  21. display: flex;
  22. flex-direction: column;
  23. align-items: center;
  24. justify-content: center;
  25. }
  26. .tv-card:hover {
  27. transform: translateY(-5px);
  28. box-shadow: 0 8px 15px rgba(0,0,0,0.1);
  29. background: white;
  30. border-color: var(--primary-color);
  31. }
  32. .tv-icon { font-size: 3em; margin-bottom: 10px; }
  33. .tv-name { font-weight: bold; color: #2c3e50; margin-bottom: 5px; }
  34. .tv-index { font-size: 0.8em; color: #999; margin-bottom: 10px; }
  35. /* Modal */
  36. .modal {
  37. display: none;
  38. position: fixed;
  39. top: 0; left: 0; width: 100%; height: 100%;
  40. background-color: rgba(0,0,0,0.5);
  41. z-index: 2000;
  42. justify-content: center;
  43. align-items: center;
  44. }
  45. .modal-content {
  46. background-color: white;
  47. padding: 30px;
  48. border-radius: 15px;
  49. width: 90%;
  50. max-width: 500px;
  51. max-height: 90vh;
  52. overflow-y: auto;
  53. position: relative;
  54. box-shadow: 0 5px 30px rgba(0,0,0,0.3);
  55. }
  56. .modal-header {
  57. display: flex;
  58. justify-content: space-between;
  59. align-items: center;
  60. margin-bottom: 20px;
  61. border-bottom: 1px solid #eee;
  62. padding-bottom: 10px;
  63. }
  64. .modal-title { font-size: 1.4em; color: #2c3e50; font-weight: bold; }
  65. .close-btn { font-size: 1.5em; cursor: pointer; color: #aaa; line-height: 1; }
  66. .close-btn:hover { color: #333; }
  67. .config-section {
  68. background: #f8f9fa;
  69. padding: 15px;
  70. border-radius: 8px;
  71. margin-bottom: 15px;
  72. border: 1px solid #eee;
  73. }
  74. @media (max-width: 768px) {
  75. .tv-grid {
  76. justify-content: center;
  77. gap: 12px;
  78. }
  79. .tv-card {
  80. width: min(100%, 160px);
  81. }
  82. .modal {
  83. align-items: flex-end;
  84. padding: 0;
  85. }
  86. .modal-content {
  87. width: 100%;
  88. max-width: none;
  89. border-radius: 16px 16px 0 0;
  90. max-height: 90vh;
  91. padding: 20px 18px 24px;
  92. padding-bottom: max(24px, env(safe-area-inset-bottom, 0px));
  93. }
  94. .close-btn {
  95. min-width: 44px;
  96. min-height: 44px;
  97. display: inline-flex;
  98. align-items: center;
  99. justify-content: center;
  100. font-size: 1.75rem;
  101. }
  102. .modal-title {
  103. font-size: 1.15rem;
  104. }
  105. .status-display .btn {
  106. min-height: 44px;
  107. padding: 0 16px !important;
  108. font-size: 14px !important;
  109. }
  110. .free-time-actions {
  111. width: 100%;
  112. }
  113. .free-time-actions .btn {
  114. flex: 1 1 auto;
  115. min-width: min(100%, 140px);
  116. }
  117. }
  118. {% endblock %}
  119. {% block content %}
  120. <div class="module-header">
  121. <h2>📺 电视控制 (Kodi)</h2>
  122. </div>
  123. <div class="sub-section">
  124. <h3>👤 控制单个电视</h3>
  125. <p style="margin-bottom: 15px; color: #666; font-size: 0.9em;">点击电视图标进行详细配置</p>
  126. <div id="tv-grid-container" class="tv-grid">
  127. <div style="width: 100%; text-align: center; padding: 20px;">
  128. 加载中...
  129. </div>
  130. </div>
  131. </div>
  132. <div class="sub-section">
  133. <h3>👥 控制所有电视</h3>
  134. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-bottom: 20px;">
  135. <h4 style="margin-bottom: 15px; color: #ff6b6b; border-left: 4px solid #ff6b6b; padding-left: 10px;">同步播放控制</h4>
  136. <div class="control-row">
  137. <div class="control-group">
  138. <label for="videoSelect">选择视频 (Video ID)</label>
  139. <select id="videoSelect">
  140. <option value="">加载中...</option>
  141. </select>
  142. </div>
  143. <div class="control-group" style="flex: 0 0 auto;">
  144. <button class="btn btn-secondary" onclick="startKodi()">同步播放视频</button>
  145. </div>
  146. </div>
  147. </div>
  148. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-bottom: 20px;">
  149. <h4 style="margin-bottom: 15px; color: #4ecdc4; border-left: 4px solid #4ecdc4; padding-left: 10px;">音量控制</h4>
  150. <div class="control-row">
  151. <div class="control-group" style="width: 100%;">
  152. <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
  153. <label for="globalVolume">全局音量</label>
  154. <span id="volumeValue" style="font-weight: bold; color: #4ecdc4;">65</span>
  155. </div>
  156. <input type="range" id="globalVolume" min="0" max="100" value="65" style="width: 100%; cursor: pointer;" onchange="setGlobalVolume()" oninput="updateVolumeDisplay(this.value)">
  157. </div>
  158. </div>
  159. </div>
  160. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee;">
  161. <h4 style="margin-bottom: 15px; color: #333; border-left: 4px solid #333; padding-left: 10px;">系统与模式控制</h4>
  162. <div class="control-row">
  163. <button class="btn btn-primary" style="flex: 1;" onclick="startAllKodiApps()">启动所有电视Kodi应用</button>
  164. <button class="btn btn-warning" style="flex: 1;" onclick="revokeIndividualState()">撤销独立控制 (恢复同步)</button>
  165. </div>
  166. <div class="control-row">
  167. <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnAllTvs()">唤醒所有电视</button>
  168. <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffAllTvs()">息屏所有电视</button>
  169. </div>
  170. </div>
  171. <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-top: 20px;">
  172. <h4 style="margin-bottom: 15px; color: #9b59b6; border-left: 4px solid #9b59b6; padding-left: 10px;">⏰ 定时闲时播放控制</h4>
  173. <p style="margin-bottom: 15px; color: #666; font-size: 0.9em;">
  174. 启用后,系统将在 07:30 - 18:00 期间自动循环播放视频。
  175. </p>
  176. <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
  177. <div>
  178. <strong>当前状态:</strong>
  179. <span id="freeTimeStatusText" style="font-weight: bold; color: #666;">加载中...</span>
  180. </div>
  181. <div class="control-row free-time-actions" style="margin-bottom: 0; gap: 10px; flex: 0 0 auto;">
  182. <button id="btnEnableFreeTime" class="btn btn-primary" onclick="controlFreeTimePlay('start')">开启功能</button>
  183. <button id="btnDisableFreeTime" class="btn btn-secondary" style="background-color: #95a5a6; display: none;" onclick="controlFreeTimePlay('stop')">关闭功能</button>
  184. </div>
  185. </div>
  186. </div>
  187. <div class="status-display" id="kodiStatusDisplay">
  188. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
  189. <strong>📊 Kodi 系统状态</strong>
  190. <button class="btn btn-warning" style="padding: 5px 10px; font-size: 12px;" onclick="getKodiStatus()">刷新</button>
  191. </div>
  192. <div id="kodiStatusContent">加载中...</div>
  193. </div>
  194. </div>
  195. <!-- 弹窗 (Modal) -->
  196. <div id="tvConfigModal" class="modal">
  197. <div class="modal-content">
  198. <div class="modal-header">
  199. <div class="modal-title">
  200. 📺 <span id="modalTvName">电视配置</span>
  201. </div>
  202. <span class="close-btn" onclick="closeTvModal()">&times;</span>
  203. </div>
  204. <!-- 电源控制 -->
  205. <div class="config-section">
  206. <div class="config-title" style="color: #ff6b6b; font-weight: bold; margin-bottom: 10px;">🔌 电源控制</div>
  207. <div class="control-row" style="margin-bottom: 0;">
  208. <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnCurrentTv()">唤醒</button>
  209. <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffCurrentTv()">息屏</button>
  210. </div>
  211. </div>
  212. <!-- 图片播放 -->
  213. <div class="config-section">
  214. <div class="config-title" style="color: #4ecdc4; font-weight: bold; margin-bottom: 10px;">🖼️ 图片播放</div>
  215. <div class="control-group" style="margin-bottom: 10px;">
  216. <label>方式一:上传图片</label>
  217. <input type="file" id="modalImageFile" accept="image/*">
  218. </div>
  219. <div class="control-group" style="margin-bottom: 10px;">
  220. <label>方式二:图片URL</label>
  221. <input type="text" id="modalImageUrl" placeholder="http://example.com/image.jpg">
  222. </div>
  223. <button class="btn btn-primary" style="width: 100%;" onclick="playImageCurrentTv()">播放图片</button>
  224. </div>
  225. <!-- RTSP播放 -->
  226. <div class="config-section">
  227. <div class="config-title" style="color: #667eea; font-weight: bold; margin-bottom: 10px;">📹 RTSP视频流</div>
  228. <div class="control-group" style="margin-bottom: 10px;">
  229. <label>RTSP URL</label>
  230. <input type="text" id="modalRtspUrl" placeholder="rtsp://example.com/stream">
  231. </div>
  232. <div class="control-group" style="margin-bottom: 10px;">
  233. <label>音量 (0-100)</label>
  234. <input type="number" id="modalRtspVolume" min="0" max="100" value="0">
  235. </div>
  236. <button class="btn btn-info" style="width: 100%;" onclick="playRtspCurrentTv()">播放RTSP流</button>
  237. </div>
  238. </div>
  239. </div>
  240. {% endblock %}
  241. {% block scripts %}
  242. <script>
  243. let currentKodiStatus = null;
  244. let activeTvIndex = null;
  245. document.addEventListener('DOMContentLoaded', function() {
  246. getKodiStatus();
  247. getKodiClients();
  248. getVideoList();
  249. getFreeTimePlayStatus();
  250. getGlobalVolume();
  251. window.onclick = function(event) {
  252. const modal = document.getElementById('tvConfigModal');
  253. if (event.target == modal) {
  254. closeTvModal();
  255. }
  256. }
  257. });
  258. async function getKodiStatus() {
  259. try {
  260. const response = await fetch('/api/kodi/status');
  261. const result = await response.json();
  262. if (result.success) {
  263. currentKodiStatus = result.data;
  264. document.getElementById('kodiStatusContent').innerHTML = `
  265. <p><strong>线程状态:</strong> ${currentKodiStatus.is_running ? '运行中' : '已停止'}</p>
  266. <p><strong>信息:</strong> ${currentKodiStatus.message}</p>
  267. `;
  268. } else {
  269. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">获取失败: ${result.message}</span>`;
  270. }
  271. } catch (error) {
  272. document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">网络错误</span>`;
  273. }
  274. }
  275. async function getKodiClients() {
  276. const container = document.getElementById('tv-grid-container');
  277. if (!container) return;
  278. try {
  279. const response = await fetch('/api/kodi/clients');
  280. const result = await response.json();
  281. if (result.success && result.data.length > 0) {
  282. container.innerHTML = '';
  283. result.data.forEach(client => {
  284. const index = client.index;
  285. const name = client.name;
  286. const cardDiv = document.createElement('div');
  287. cardDiv.className = 'tv-card';
  288. cardDiv.onclick = () => openTvModal(index, name);
  289. cardDiv.innerHTML = `
  290. <div class="tv-icon">📺</div>
  291. <div class="tv-name">${name}</div>
  292. <div class="tv-index">ID: ${index}</div>
  293. <button class="btn btn-primary" style="padding: 5px 15px; font-size: 12px; margin-top: 5px;">
  294. ⚙️ 配置
  295. </button>
  296. `;
  297. container.appendChild(cardDiv);
  298. });
  299. } else {
  300. container.innerHTML = '<div style="width: 100%; text-align: center; color: #666;">未找到 Kodi 客户端</div>';
  301. }
  302. } catch (error) {
  303. console.error(error);
  304. container.innerHTML = '<div style="width: 100%; text-align: center; color: red;">加载失败</div>';
  305. }
  306. }
  307. async function getVideoList() {
  308. const selectEl = document.getElementById('videoSelect');
  309. try {
  310. const response = await fetch('/api/kodi/videos');
  311. const result = await response.json();
  312. if (result.success && result.data.length > 0) {
  313. selectEl.innerHTML = '';
  314. result.data.forEach(video => {
  315. const option = document.createElement('option');
  316. option.value = video.id;
  317. option.textContent = `${video.name} (ID: ${video.id})`;
  318. selectEl.appendChild(option);
  319. });
  320. } else {
  321. selectEl.innerHTML = '<option value="">未找到视频</option>';
  322. }
  323. } catch (error) {
  324. console.error(error);
  325. selectEl.innerHTML = '<option value="">加载失败</option>';
  326. }
  327. }
  328. async function startKodi() {
  329. const videoId = parseInt(document.getElementById('videoSelect').value);
  330. if (isNaN(videoId) || videoId < 0) {
  331. showMessage('请选择有效的视频', 'error');
  332. return;
  333. }
  334. try {
  335. showLoading(true);
  336. const response = await fetch('/api/kodi/start', {
  337. method: 'POST',
  338. headers: { 'Content-Type': 'application/json' },
  339. body: JSON.stringify({ video_id: videoId })
  340. });
  341. const result = await response.json();
  342. if (result.success) {
  343. showMessage(result.message);
  344. getKodiStatus();
  345. } else {
  346. showMessage(result.message, 'error');
  347. }
  348. } catch (error) {
  349. showMessage('网络错误: ' + error.message, 'error');
  350. } finally {
  351. showLoading(false);
  352. }
  353. }
  354. function updateVolumeDisplay(val) {
  355. document.getElementById('volumeValue').textContent = val;
  356. }
  357. async function getGlobalVolume() {
  358. try {
  359. const response = await fetch('/api/kodi/get_volume');
  360. const result = await response.json();
  361. if (result.success) {
  362. const vol = result.data.volume;
  363. document.getElementById('globalVolume').value = vol;
  364. updateVolumeDisplay(vol);
  365. }
  366. } catch (error) {
  367. console.error('获取音量失败', error);
  368. }
  369. }
  370. async function setGlobalVolume() {
  371. const volume = parseInt(document.getElementById('globalVolume').value);
  372. if (isNaN(volume) || volume < 0 || volume > 100) {
  373. showMessage('音量必须是 0-100 之间的整数', 'error');
  374. return;
  375. }
  376. try {
  377. // 滑动条体验优化:不显示全屏loading
  378. const response = await fetch('/api/kodi/set_volume', {
  379. method: 'POST',
  380. headers: { 'Content-Type': 'application/json' },
  381. body: JSON.stringify({ volume: volume })
  382. });
  383. const result = await response.json();
  384. if (result.success) {
  385. showMessage(result.message);
  386. } else {
  387. showMessage(result.message, 'error');
  388. }
  389. } catch (error) {
  390. showMessage('网络错误: ' + error.message, 'error');
  391. }
  392. }
  393. async function revokeIndividualState() {
  394. try {
  395. showLoading(true);
  396. const response = await fetch('/api/kodi/revoke_individual_state', {
  397. method: 'POST',
  398. headers: { 'Content-Type': 'application/json' }
  399. });
  400. const result = await response.json();
  401. if (result.success) {
  402. showMessage(result.message);
  403. getKodiStatus();
  404. } else {
  405. showMessage(result.message, 'error');
  406. }
  407. } catch (error) {
  408. showMessage('网络错误: ' + error.message, 'error');
  409. } finally {
  410. showLoading(false);
  411. }
  412. }
  413. async function startAllKodiApps() {
  414. try {
  415. showLoading(true);
  416. const response = await fetch('/api/kodi/start_all_apps', {
  417. method: 'POST',
  418. headers: { 'Content-Type': 'application/json' }
  419. });
  420. const result = await response.json();
  421. if (result.success) {
  422. showMessage(result.message);
  423. getKodiStatus();
  424. } else {
  425. showMessage(result.message, 'error');
  426. }
  427. } catch (error) {
  428. showMessage('网络错误: ' + error.message, 'error');
  429. } finally {
  430. showLoading(false);
  431. }
  432. }
  433. function openTvModal(index, name) {
  434. activeTvIndex = index;
  435. document.getElementById('modalTvName').textContent = `${name} (Index: ${index})`;
  436. document.getElementById('modalImageFile').value = '';
  437. document.getElementById('modalImageUrl').value = '';
  438. document.getElementById('modalRtspUrl').value = '';
  439. document.getElementById('modalRtspVolume').value = '0';
  440. document.getElementById('tvConfigModal').style.display = 'flex';
  441. }
  442. function closeTvModal() {
  443. document.getElementById('tvConfigModal').style.display = 'none';
  444. activeTvIndex = null;
  445. }
  446. async function turnOnCurrentTv() {
  447. if (activeTvIndex === null) return;
  448. try {
  449. showLoading(true);
  450. const response = await fetch('/api/mitv/turn_on', {
  451. method: 'POST',
  452. headers: { 'Content-Type': 'application/json' },
  453. body: JSON.stringify({ kodi_id: activeTvIndex })
  454. });
  455. const result = await response.json();
  456. if (result.success) {
  457. showMessage(result.message);
  458. } else {
  459. showMessage(result.message, 'error');
  460. }
  461. } catch (error) {
  462. showMessage('网络错误: ' + error.message, 'error');
  463. } finally {
  464. showLoading(false);
  465. }
  466. }
  467. async function turnOffCurrentTv() {
  468. if (activeTvIndex === null) return;
  469. try {
  470. showLoading(true);
  471. const response = await fetch('/api/mitv/turn_off', {
  472. method: 'POST',
  473. headers: { 'Content-Type': 'application/json' },
  474. body: JSON.stringify({ kodi_id: activeTvIndex })
  475. });
  476. const result = await response.json();
  477. if (result.success) {
  478. showMessage(result.message);
  479. } else {
  480. showMessage(result.message, 'error');
  481. }
  482. } catch (error) {
  483. showMessage('网络错误: ' + error.message, 'error');
  484. } finally {
  485. showLoading(false);
  486. }
  487. }
  488. async function playImageCurrentTv() {
  489. if (activeTvIndex === null) return;
  490. const fileInput = document.getElementById('modalImageFile');
  491. const urlInput = document.getElementById('modalImageUrl').value.trim();
  492. if ((!fileInput.files || fileInput.files.length === 0) && !urlInput) {
  493. showMessage('请选择图片文件或输入图片URL', 'error');
  494. return;
  495. }
  496. try {
  497. showLoading(true);
  498. let response;
  499. if (fileInput.files && fileInput.files.length > 0) {
  500. const formData = new FormData();
  501. formData.append('file', fileInput.files[0]);
  502. formData.append('kodi_client_index', activeTvIndex);
  503. response = await fetch('/api/kodi/play_image', {
  504. method: 'POST',
  505. body: formData
  506. });
  507. } else {
  508. response = await fetch('/api/kodi/play_image', {
  509. method: 'POST',
  510. headers: { 'Content-Type': 'application/json' },
  511. body: JSON.stringify({
  512. image_url: urlInput,
  513. kodi_client_index: activeTvIndex
  514. })
  515. });
  516. }
  517. const result = await response.json();
  518. if (result.success) {
  519. showMessage(result.message);
  520. } else {
  521. showMessage(result.message, 'error');
  522. }
  523. } catch (error) {
  524. showMessage('网络错误: ' + error.message, 'error');
  525. } finally {
  526. showLoading(false);
  527. }
  528. }
  529. async function playRtspCurrentTv() {
  530. if (activeTvIndex === null) return;
  531. const rtspUrl = document.getElementById('modalRtspUrl').value.trim();
  532. const volume = parseInt(document.getElementById('modalRtspVolume').value);
  533. if (!rtspUrl) {
  534. showMessage('请输入RTSP视频流URL', 'error');
  535. return;
  536. }
  537. if (isNaN(volume) || volume < 0 || volume > 100) {
  538. showMessage('音量必须是0-100之间的整数', 'error');
  539. return;
  540. }
  541. try {
  542. showLoading(true);
  543. const response = await fetch('/api/kodi/play_rtsp', {
  544. method: 'POST',
  545. headers: { 'Content-Type': 'application/json' },
  546. body: JSON.stringify({
  547. rtsp_url: rtspUrl,
  548. kodi_client_index: activeTvIndex,
  549. volume: volume
  550. })
  551. });
  552. const result = await response.json();
  553. if (result.success) {
  554. showMessage(result.message);
  555. } else {
  556. showMessage(result.message, 'error');
  557. }
  558. } catch (error) {
  559. showMessage('网络错误: ' + error.message, 'error');
  560. } finally {
  561. showLoading(false);
  562. }
  563. }
  564. async function turnOnAllTvs() {
  565. if (!confirm('确定要唤醒所有电视吗?')) return;
  566. try {
  567. showLoading(true);
  568. const response = await fetch('/api/mitv/turn_on_all', {
  569. method: 'POST',
  570. headers: { 'Content-Type': 'application/json' }
  571. });
  572. const result = await response.json();
  573. if (result.success) {
  574. showMessage(result.message);
  575. } else {
  576. showMessage(result.message, 'error');
  577. }
  578. } catch (error) {
  579. showMessage('网络错误: ' + error.message, 'error');
  580. } finally {
  581. showLoading(false);
  582. }
  583. }
  584. async function turnOffAllTvs() {
  585. if (!confirm('确定要息屏所有电视吗?')) return;
  586. try {
  587. showLoading(true);
  588. const response = await fetch('/api/mitv/turn_off_all', {
  589. method: 'POST',
  590. headers: { 'Content-Type': 'application/json' }
  591. });
  592. const result = await response.json();
  593. if (result.success) {
  594. showMessage(result.message);
  595. } else {
  596. showMessage(result.message, 'error');
  597. }
  598. } catch (error) {
  599. showMessage('网络错误: ' + error.message, 'error');
  600. } finally {
  601. showLoading(false);
  602. }
  603. }
  604. async function getFreeTimePlayStatus() {
  605. const statusSpan = document.getElementById('freeTimeStatusText');
  606. const btnEnable = document.getElementById('btnEnableFreeTime');
  607. const btnDisable = document.getElementById('btnDisableFreeTime');
  608. if (!statusSpan) return;
  609. try {
  610. const response = await fetch('/api/kodi/free_time/status');
  611. const result = await response.json();
  612. if (result.success) {
  613. const isEnabled = result.data.enabled;
  614. if (isEnabled) {
  615. statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
  616. btnEnable.style.display = 'none';
  617. btnDisable.style.display = 'inline-block';
  618. } else {
  619. statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
  620. btnEnable.style.display = 'inline-block';
  621. btnDisable.style.display = 'none';
  622. }
  623. } else {
  624. statusSpan.textContent = '获取失败';
  625. }
  626. } catch (error) {
  627. console.error(error);
  628. statusSpan.textContent = '网络错误';
  629. }
  630. }
  631. async function controlFreeTimePlay(action) {
  632. try {
  633. showLoading(true);
  634. const response = await fetch('/api/kodi/free_time/control', {
  635. method: 'POST',
  636. headers: { 'Content-Type': 'application/json' },
  637. body: JSON.stringify({ action: action })
  638. });
  639. const result = await response.json();
  640. if (result.success) {
  641. showMessage(result.message);
  642. // 更新状态
  643. if (result.data) {
  644. const isEnabled = result.data.enabled;
  645. const statusSpan = document.getElementById('freeTimeStatusText');
  646. const btnEnable = document.getElementById('btnEnableFreeTime');
  647. const btnDisable = document.getElementById('btnDisableFreeTime');
  648. if (isEnabled) {
  649. statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
  650. btnEnable.style.display = 'none';
  651. btnDisable.style.display = 'inline-block';
  652. } else {
  653. statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
  654. btnEnable.style.display = 'inline-block';
  655. btnDisable.style.display = 'none';
  656. }
  657. } else {
  658. getFreeTimePlayStatus();
  659. }
  660. } else {
  661. showMessage(result.message, 'error');
  662. }
  663. } catch (error) {
  664. showMessage('网络错误: ' + error.message, 'error');
  665. } finally {
  666. showLoading(false);
  667. }
  668. }
  669. </script>
  670. {% endblock %}