diff options
Diffstat (limited to 'src/popup')
| -rw-r--r-- | src/popup/popup.css | 77 | ||||
| -rw-r--r-- | src/popup/popup.html | 34 | ||||
| -rw-r--r-- | src/popup/popup.js | 134 |
3 files changed, 245 insertions, 0 deletions
diff --git a/src/popup/popup.css b/src/popup/popup.css new file mode 100644 index 0000000..8ed312a --- /dev/null +++ b/src/popup/popup.css @@ -0,0 +1,77 @@ +body { + width: 260px; + margin: 0; + padding: 12px; + font-family: system-ui, sans-serif; + font-size: 13px; + color: #1f1f1f; + background: #ffffff; +} + +h1 { + font-size: 14px; + margin: 0 0 10px; +} + +.row { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 10px; +} + +.toggle-label { + font-weight: 500; +} + +.toggle-label + input[type='checkbox'] { + margin-left: 8px; +} + +.row .toggle-label { + display: inline-block; + margin-bottom: 4px; +} + +label { + font-weight: 500; +} + +select, +button { + font-family: system-ui, sans-serif; + font-size: 13px; +} + +.model-row { + display: flex; + gap: 4px; +} + +.model-row select { + flex: 1; + min-width: 0; +} + +.status { + color: #5f6368; + font-size: 11px; + margin: 4px 0 0; +} + +.status.error { + color: #c5221f; +} + +#open-options { + width: 100%; + padding: 6px; + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 4px; + background: #fff; + cursor: pointer; +} + +#open-options:hover { + background: #f0f4ff; +}
\ No newline at end of file diff --git a/src/popup/popup.html b/src/popup/popup.html new file mode 100644 index 0000000..92099a7 --- /dev/null +++ b/src/popup/popup.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="ja"> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" href="popup.css"> +</head> +<body> + <main> + <h1>Ollama Translations</h1> + + <section class="row"> + <label class="toggle-label" for="enabled-toggle">翻訳機能</label> + <input type="checkbox" id="enabled-toggle" role="switch"> + </section> + + <section class="row"> + <label for="model-select">モデル</label> + <div class="model-row"> + <select id="model-select"></select> + <button id="reload-models" type="button" title="モデル一覧を再取得">↻</button> + </div> + <p id="model-status" class="status" hidden></p> + </section> + + <section class="row"> + <label for="language-select">翻訳先言語</label> + <select id="language-select"></select> + </section> + + <button id="open-options" type="button">詳細設定を開く</button> + </main> + <script src="popup.js"></script> +</body> +</html>
\ No newline at end of file diff --git a/src/popup/popup.js b/src/popup/popup.js new file mode 100644 index 0000000..c07851e --- /dev/null +++ b/src/popup/popup.js @@ -0,0 +1,134 @@ +'use strict'; + +import { LANGUAGES, ERROR_CODES, RUNTIME_MESSAGES } from '../shared/constants.js'; +import { defaultSettings } from '../shared/settings-schema.js'; + +const $enabled = /** @type {HTMLInputElement} */ (document.getElementById('enabled-toggle')); +const $model = /** @type {HTMLSelectElement} */ (document.getElementById('model-select')); +const $reload = /** @type {HTMLButtonElement} */ (document.getElementById('reload-models')); +const $modelStatus = /** @type {HTMLParagraphElement} */ (document.getElementById('model-status')); +const $language = /** @type {HTMLSelectElement} */ (document.getElementById('language-select')); +const $openOptions = /** @type {HTMLButtonElement} */ (document.getElementById('open-options')); + +/** @type {string[]} current model list for change detection */ +let knownModels = []; + +function fillLanguages(selected) { + $language.textContent = ''; + for (const lang of LANGUAGES) { + const option = document.createElement('option'); + option.value = lang.code; + option.textContent = lang.label; + if (lang.code === selected) option.selected = true; + $language.append(option); + } +} + +function fillModels(models, selected) { + $model.textContent = ''; + if (models.length === 0) { + const option = document.createElement('option'); + option.value = ''; + option.textContent = '(モデルなし / Ollama未起動)'; + $model.append(option); + } + for (const name of models) { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + if (name === selected) option.selected = true; + $model.append(option); + } + if (selected && !models.includes(selected)) { + const option = document.createElement('option'); + option.value = selected; + option.textContent = `${selected}(一覧にありません)`; + option.selected = true; + $model.append(option); + } +} + +function showStatus(message, isError) { + $modelStatus.hidden = false; + $modelStatus.textContent = message; + $modelStatus.classList.toggle('error', Boolean(isError)); +} + +async function loadModels() { + showStatus('モデル一覧を取得中…', false); + try { + const response = await browser.runtime.sendMessage({ type: RUNTIME_MESSAGES.LIST_MODELS }); + if (response && response.ok && Array.isArray(response.models)) { + knownModels = response.models; + const current = $model.value; + fillModels(response.models, current); + if (response.models.length === 0) { + $modelStatus.hidden = true; + } else { + $modelStatus.hidden = true; + } + } else { + const code = response && response.error; + if (code === ERROR_CODES.OLLAMA_UNAVAILABLE) { + showStatus('Ollamaに接続できません。', true); + } else { + showStatus('モデル一覧の取得に失敗しました。', true); + } + fillModels([], $model.value); + } + } catch { + showStatus('モデル一覧の取得に失敗しました。', true); + } +} + +async function loadSettings() { + const result = await browser.storage.local.get({ settings: null }); + const settings = result.settings || defaultSettings(); + $enabled.checked = Boolean(settings.enabled); + $language.value = settings.targetLanguage || 'ja'; + fillModels(knownModels, settings.model || ''); + if (settings.model) { + // Preserve the stored model even before the first list load. + fillModels(knownModels.length ? knownModels : [settings.model], settings.model); + } +} + +async function persist(patch) { + const result = await browser.storage.local.get({ settings: null }); + const settings = result.settings || defaultSettings(); + const merged = { ...settings, ...patch }; + await browser.storage.local.set({ settings: merged }); +} + +$enabled.addEventListener('change', () => { + void persist({ enabled: $enabled.checked }); +}); + +$model.addEventListener('change', () => { + const value = $model.value; + if (value) void persist({ model: value }); +}); + +$language.addEventListener('change', () => { + void persist({ targetLanguage: $language.value }); +}); + +$reload.addEventListener('click', () => { + void loadModels(); +}); + +$openOptions.addEventListener('click', () => { + browser.runtime.openOptionsPage(); +}); + +// Init +for (const language of LANGUAGES) { + const option = document.createElement('option'); + option.value = language.code; + option.textContent = language.label; + $language.append(option); +} +void (async () => { + await loadSettings(); + await loadModels(); +})();
\ No newline at end of file |
