Расширения Chrome для начинающих. Часть 2. Практика

от Эни Оньедикачи

В этой части мы создадим расширение таймера Pomodoro, используя API, о которых мы узнали в части 1. Стилизация будет выполняться с помощью CSS. Полный код этого проекта можно найти на GitHub. Посмотреть демо на Youtube.

Манифест и всплывающее окно

Как вы уже знаете, при создании расширений первым файлом, который вы создаете, является файл манифеста.

Создайте новый файл manifest.json.

📦 Chrome-Extension-Series 
┣ 🎨 icon.png 
┣ 📄 manifest.json
{
    "manifest_version": 3,
    "name": "Pomodoro Timer",
    "version": "1.0",
    "description": "Assists you to focus and get things done",
    "icons": {
        "16": "icon.png",
        "48": "icon.png",
        "128": "icon.png"
    },
    "action": {
        "default_icon": "icon.png",
        "default_title": "Pomodoro Timer",
        "default_popup": "popup/popup.html"
    }
}

Теперь у нас есть набор расширений, давайте создадим всплывающую страницу. Файл popup.html помещается в папку popup, чтобы добавить структуру нашему проекту.

📦 Chrome-Extension-Series 
┣ 🎨 icon.png 
┣ 📄 manifest.json 
┣ 📂 popup 
┃ ┣ 📄 popup.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./popup.css">
    <title>Pomodoro Timer</title>
</head>
<body>
    <div class="header">
        <img src="../icon.png">
    </div>
    <h1>00:00</h1>
    <button>Start Timer</button>
    <button>Add Task</button>
    <div>
        <input type="text">
        <input type="button" value="X">
    </div>
</body>
<script src="popup.js"></script>
</html>

В предыдущем коде определена структура нашей всплывающей страницы. Он связан с pop.css для стиля и pop.js для интерактивности.

Давайте добавим немного стиля и создадим файл popup.css, с которым связан наш popup.html.

📦 Chrome-Extension-Series 
┣ 🎨 icon.png 
┣ 📄 manifest.json 
┣ 📂 popup 
┣ 📄 popup.css
body {
    height: 400px;
    width: 300px;
}

.header {
    display: flex;
    justify-content: center;
    height: 40px;
}

Перезагрузите страницу расширения в браузере и щелкните всплывающее окно, отобразится popup.html.

Функция списка задач

Функция списка задач позволит нам добавлять и удалять задачи.

Добавление задач

In popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./popup.css">
    <title>Pomodoro Timer</title>
</head>
<body>
    <div class="header">
        <img src="../icon.png">
    </div>
    <h1>00:00</h1>
    <button>Start Timer</button>
    <button id="add-task-btn">Add Task</button>
+    <div id="task-container">
        <input type="text">
        <input type="button" value="X">
+    </div>
</body>
<script src="popup.js"></script>
</html>

In popup.js

📦 Chrome-Extension-Series
 ┣ 🎨 icon.png
 ┣ 📄 manifest.json
 ┣ 📂 popup
 ┣ 📄 popup.css
 ┣ 📄 popup.js
const addTaskBtn = document.getElementById('add-task-btn')
addTaskBtn.addEventListener('click', () => addTask())

function addTask() {
  const taskRow = document.createElement('div')

  // Create text input
  const text = document.createElement('input')
  text.type = 'text'
  text.placeholder = 'Enter a task..'

  // Create delete button
  const deleteBtn = document.createElement('input')
  deleteBtn.type = 'button'
  deleteBtn.value = 'X'

  // append input elements to taskRow
  taskRow.appendChild(text)
  taskRow.appendChild(deleteBtn)

  // append taskRow to taskContainer
  const taskContainer = document.getElementById('task-container')
  taskContainer.appendChild(taskRow)
}

В предыдущем коде мы выбираем кнопку Add Task через ее id, добавляем прослушиватель событий click и функцию обратного вызова, которая добавляет новую задачу в пользовательский интерфейс.

Удаление задач

In popup.js

- const addTaskBtn = document.getElementById('add-task-btn')
addTaskBtn.addEventListener('click', () => addTask())

- function addTask() {
  const taskRow = document.createElement('div')

  // Create text input
-  const text = document.createElement('input')
  text.type = 'text'
  text.placeholder = 'Enter a task..'

  // Create delete button
-  const deleteBtn = document.createElement('input')
  deleteBtn.type = 'button'
  deleteBtn.value = 'X'

  // append input elements to taskRow
  taskRow.appendChild(text)
  taskRow.appendChild(deleteBtn)

  // append taskRow to taskContainer
-  const taskContainer = document.getElementById('task-container')
  taskContainer.appendChild(taskRow)
}

// array to store tasks
let tasks = []

const addTaskBtn = document.getElementById('add-task-btn')
addTaskBtn.addEventListener('click', () => addTask())

// render tasks
function renderTask(taskNum) {
  const taskRow = document.createElement('div')

  // Create text input
  const text = document.createElement('input')
  text.type = 'text'
  text.placeholder = 'Enter a task..'

  //Set and track input values of tasks in the array
  text.value = tasks[taskNum]
  text.addEventListener('change', () => {
    tasks[tasksNum] = text.value
  })

  // Create delete button
  const deleteBtn = document.createElement('input')
  deleteBtn.type = 'button'
  deleteBtn.value = 'X'

  // delete task
  deleteBtn.addEventListener('click', () => {
    deleteTask(taskNum)
  })

  // append input elements to taskRow
  taskRow.appendChild(text)
  taskRow.appendChild(deleteBtn)

  // append taskRow to taskContainer
  const taskContainer = document.getElementById('task-container')
  taskContainer.appendChild(taskRow)
}

function addTask() {
  const tasksNum = tasks.length
  // add tasks to array
  tasks.push('')
  renderTask(tasksNum)
}

// delete and re-render tasks after mutation
function deleteTask(tasksNum) {
  tasks.splice(tasksNum, 1)
  renderTasks()
}

function renderTasks() {
  const taskContainer = document.getElementById('task-container')
  taskContainer.textContent = ''
  tasks.forEach((taskText, tasksNum) => {
    renderTask(tasksNum)
  })
}

Мы внесли серьезные изменения в файл popup.js в предыдущем коде. Давайте разбираться, что происходит:

  • В основном, мы добавляем и удаляем задачи
  • Массив (tasks) создается, чтобы мы могли хранить задачи
  • Функция rendTask() создает новую задачу и отображает ее в DOM (объектная модель документа) при нажатии кнопки Add Task.
  • Функция addTask() является обработчиком событий для кнопки Add Task.
  • Функция deleteTask() удаляет задачи при нажатии кнопки удаления задачи (X).
  • Функция renderTasks() обновляет массив задач всякий раз, когда задача удаляется, т. е. повторно отображает пользовательский интерфейс.

Теперь, если мы проверим наше расширение, мы можем добавлять и удалять задачи, но данные не являются постоянными — нам нужно реализовать хранилище.

Хранение задач

Во-первых, мы установили необходимые разрешения для использования API хранилища в manifest.json.

{
    "manifest_version": 3,
    "name": "Pomodoro Timer",
    "version": "1.0",
    "description": "Assists you to focus and get things done",
    "icons": {
        "16": "icon.png",
        "48": "icon.png",
        "128": "icon.png"
    },
    "action": {
        "default_icon": "icon.png",
        "default_title": "Pomodoro Timer",
        "default_popup": "popup/popup.html"
    },
+ "permissions": ["storage"]
}

In popup.js

// array to store tasks
let tasks = []

const addTaskBtn = document.getElementById('add-task-btn')
addTaskBtn.addEventListener('click', () => addTask())

// set default storage value for the tasks
chrome.storage.sync.get(['tasks'], (res) => {
  tasks = res.tasks ? res.tasks : []
  renderTasks()
})

// save tasks
function saveTasks() {
  chrome.storage.sync.set({
    tasks: tasks,
  })
}

// render tasks
function renderTask(taskNum) {
  const taskRow = document.createElement('div')

  // Create text input
  const text = document.createElement('input')
  text.type = 'text'
  text.placeholder = 'Enter a task..'

  //Set and track input values of tasks in the array
  text.value = tasks[taskNum]
  text.addEventListener('change', () => {
    tasks[taskNum] = text.value

    // call saveTask whenever a value changes
   saveTasks()
  })

  ....

  function addTask() {
  const tasksNum = tasks.length
  // add tasks to array
  tasks.push('')
  renderTask(tasksNum)
  saveTasks()    
}

  // delete and re-render tasks after mutation
function deleteTask(tasksNum) {
  tasks.splice(tasksNum, 1)
  renderTasks()
   saveTasks()
}

Мы используем API хранилища Chrome в предыдущем коде для хранения данных нашего расширения.

  • Данные по умолчанию для расширения изначально устанавливаются в пустой массив, если в массиве задач нет задач для рендеринга.
  • Функция saveTasks() сохраняет наш массив task в API хранилища.
  • В renderTask() всякий раз, когда задача добавляется или удаляется, она сохраняется через saveTasks(), и то же самое касается addTask() и deleteTask().

Функция задачи завершена; мы можем удалять, добавлять и сохранять задачи.

Функция таймера

Функция таймера потребует от нас создания фонового сценария и использования сигналов тревоги и уведомлений, чтобы уведомлять пользователя, когда таймер истекает.

Таймер запуска и паузы

Давайте установим необходимые разрешения в manifest.json.

{
    "manifest_version": 3,
    "name": "Pomodoro Timer",
    "version": "1.0",
    "description": "Assists you to focus and get things done",
    "icons": {
        "16": "icon.png",
        "48": "icon.png",
        "128": "icon.png"
    },
    "action": {
        "default_icon": "icon.png",
        "default_title": "Pomodoro Timer",
        "default_popup": "popup/popup.html"
    },
+ "permissions": ["storage", "alarms", "notifications"],
+    "background": {
        "service_worker": "background.js"
    }
}

Создайте файл background.js для нашего фонового сценария.

📦 Chrome-Extension-Series 
┣ 🎨 icon.png 
┣ 📄 manifest.json 
┣ 📂 popup 
┣ 📄 popup.css 
┣ 📄 popup.js 
┣ 📄 background.js
// create an alarm to notify user when time is up
chrome.alarms.create("pomodoroTimer", {
    periodInMinutes: 1 / 60
})

// alarm listener
chrome.alarms.onAlarm.addListener((alarm) => {
    if (alarm.name === "pomodoroTimer") {
        chrome.storage.local.get(["timer", "isRunning"], (res) => {
            if (res.isRunning) {
                let timer = res.timer + 1
                console.log(timer)
                chrome.storage.local.set({
                    timer,
                })
            }
        })
    }
})

// storage to set and track timer variables on load
chrome.storage.local.get(["timer", "isRunning"], (res) => {
    chrome.storage.local.set({
        timer: "timer" in res ? res.timer : 0,
        isRunning: "isRunning" in res ? res.isRunning : false,
    })
})

In popup.js

// array to store tasks
let tasks = []

// Start Timer Button
+ const startTimerBtn = document.getElementById("start-timer-btn");
startTimerBtn.addEventListener("click", () => {
  chrome.storage.local.get(["isRunning"], (res) => {
    chrome.storage.local.set({
      isRunning: !res.isRunning,
    }, () => {
      startTimerBtn.textContent = !res.isRunning ? "Pause Timer" : "Start Timer"
    })
  })
})

В предыдущем коде:

  • Мы устанавливаем будильник, который срабатывает при нажатии кнопки Start Timer.
  • Переменные timer и isRunning используются для отслеживания времени и состояния таймера, они хранятся как исходные данные приложения в хранилище.
  • Мы слушаем (onAlarm.addListener) тревогу и увеличиваем timer, когда isRunning равно true, тогда timer регистрируется в консоли.
  • Наконец, в popup.js мы прослушиваем событие click на кнопке Start Timer и получаем текущее значение isRunning. Если текущее значение равно true,, оно устанавливается на false, и таймер приостанавливается; если это false,, то для сброса таймера установлено значение true.

Сбросить таймер

Теперь давайте поработаем над функцией сброса таймера. Создайте разметку для кнопки сброса popup.html.

<body>
    <div class="header">
        <img src="../icon.png">
    </div>
    <h1>00:00</h1>
    <button id="start-timer-btn">Start Timer</button>
+    <button id="reset-timer-btn">Reset Timer</button>
    <button id="add-task-btn">Add Task</button>
    <div id="task-container">
        <input type="text">
        <input type="button" value="X">
    </div>
</body>

In popup.js

// Reset Timer Button
const resetTimerBtn = document.getElementById("reset-timer-btn")
resetTimerBtn.addEventListener("click", () => {
  chrome.storage.local.set({
    // reset variables
    timer: 0,
    isRunning: false
  }, () => {
    // reset start button text-content
    startTimerBtn.textContent = "Start Timer"
  })
})

В предыдущем коде мы сделали следующее:

  • Выберите кнопку Reset Timer в DOM через ее id.
  • Добавьте к кнопке прослушиватель событий click с функцией обратного вызова, которая сбрасывает переменные timer и isRunning в хранилище.
  • Наконец, текст кнопки Start Timer устанавливается на строку «Запуск таймера», исходя из предположения, что в настоящее время это «Таймер паузы».

Отображение времени во всплывающем окне

До сих пор мы регистрировали значения наших таймеров на консоли. Давайте отобразим его на всплывающей странице.

In popup.html

<body>
    <div class="header">
        <img src="../icon.png">
    </div>
+    <h1 id="time">00:00</h1>
    <button id="start-timer-btn">Start Timer</button>
    <button id="reset-timer-btn">Reset Timer</button>
    <button id="add-task-btn">Add Task</button>
    <div id="task-container">
        <input type="text">
        <input type="button" value="X">
    </div>
</body>

In popup.js

// array to store tasks
let tasks = [];

const time = document.getElementById("time");

// Update time every 1sec
function updateTime() {
  chrome.storage.local.get(["timer"], (res) => {
    const time = document.getElementById("time")

    // get no. of minutes & secs
    const minutes = `${25 - Math.ceil(res.timer / 60)}`.padStart(2, "0");
    let seconds = "00";
    if (res.timer % 60 != 0) {
      seconds = `${60 -res.timer % 60}`.padStart(2, "0");
    }

    // show minutes & secs on UI
  time.textContent = `${minutes}:${seconds}`
  })
}

updateTime()
setInterval(updateTime, 1000)

// Start Timer Button

В предыдущем коде мы отображаем время на всплывающей странице и обновляем его при нажатии любой из кнопок, влияющих на таймер. Давайте лучше разберем код:

  • В функции updateTime() делается немного математики; значение таймера берется из хранилища.
  • Минуты для таймера получаются (таймер должен отсчитывать от 25 минут) и сохраняются в переменной minute. '25 - res.timer/60- for example, if our timer value(res.timer) was120secs,120/60=2, then '25 - 2 = 23, т.е. на часах останется 23 минуты.
  • Чтобы получить секунды, мы делим значение таймера (res.timer) на 60.
  • Значения minutes и seconds отображаются в пользовательском интерфейсе через time.textContent.
  • updateTime() вызывается автоматически при загрузке всплывающего окна и вызывается каждые 1sec пользователем setInterval.

Теперь время можно увидеть во всплывающем окне.

Отправка уведомления

Теперь давайте настроим уведомления, чтобы уведомлять пользователя, когда время истекло.

In background.js

// alarm listener
chrome.alarms.onAlarm.addListener((alarm) => {
    if (alarm.name === "pomodoroTimer") {
        chrome.storage.local.get(["timer", "isRunning"], (res) => {
            if (res.isRunning) {
                let timer = res.timer + 1
                let isRunning = true
                if(timer === 25) {
                this.registration.showNotification('Pomodoro Timer', {
                    body: "25 minutes has passed",
                    icon: "icon.png"
                })
                timer = 0
                isRunning = false

               }
                chrome.storage.local.set({
                    timer,
                    isRunning,
                })
            }
        })
    }
})

В предыдущем коде оператор if используется для проверки того, истек ли наш таймер на 25 минут, после чего регистрируется уведомление. Когда таймер истекает, значение timer сбрасывается на 0, а isRunning на false, чтобы приостановить таймер. Чтобы проверить эту функциональность, установите значение таймера по умолчанию на 10secs (помните, что это только для целей тестирования, поэтому мы не ждем 25 минут). В операторе if в приведенном выше коде измените значение таймера на 10secs- if(timer === 10). Теперь перезапустите таймер, и после 10secs вы увидите уведомление.

Повтор сеанса с открытым исходным кодом

OpenReplay – это пакет для воспроизведения сеансов с открытым исходным кодом, который позволяет вам видеть, что пользователи делают в вашем веб-приложении, помогая вам быстрее устранять неполадки. OpenReplay размещается на собственном сервере для полного контроля над вашими данными.

Начните получать удовольствие от отладки — начните использовать OpenReplay бесплатно.

Страница параметров

Теперь у нас есть базовая функциональность для расширения: мы можем запускать таймер, приостанавливать таймер, сбрасывать таймер, а также удалять и добавлять задачи. Теперь давайте сделаем расширение более настраиваемым, чтобы пользователи могли адаптировать его к своим потребностям. Некоторые пользователи могут захотеть сосредоточиться на более длительных или более коротких периодах времени. Мы должны создать страницу параметров, чтобы пользователь мог настроить расширение. Пользователь сможет установить максимальное время сеанса на 1 час (60 минут) и минимальное на 1 минуту.

Вариант настройки и хранения

Добавьте "options_pagefile in `manifest.json.`

{
    "manifest_version": 3,
    "name": "Pomodoro Timer",
    "version": "1.0",
    "description": "Assists you to focus and get things done",
    "icons": {
        "16": "icon.png",
        "48": "icon.png",
        "128": "icon.png"
    },
    "action": {
        "default_icon": "icon.png",
        "default_title": "Pomodoro Timer",
        "default_popup": "popup/popup.html"
    },
    "permissions": ["storage", "alarms", "notifications"],
    "background": {
        "service_worker": "background.js"
    },
    "options_page": "options/options.html"
}

Файл options.html помещается в папку, чтобы добавить структуру нашему проекту.

Создайте папку options и добавьте в нее файлы options.html и options.css.

📦 Chrome-Extension-Series
 ┣ 🎨 icon.png
 ┣ 📄 manifest.json
 ┣ 📂 popup
 ┣ 📂 options
 ┃   ┣ 📄 options.css
 ┃   ┣ 📄 options.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
   `<link rel="stylesheet" href="options.css">
    <title>Pomodoro Timer Extension Options</title>
</head>
<body>
    <h1>Pomodoro Timer Options</h1>
    <input id="time-option" type="number" min="1" max="60" value="25">
    <button id="save-btn">Save Options</button>
</body>
<script src="options.js"></script>
</html>

В HTML у нас есть числовое поле input с минимальным значением 1(1 минута) и максимальным значением 60(60 минут). Атрибут value содержит значение таймера по умолчанию 25 (25 минут). Кнопка Save options позволит нам сохранить параметры.

In options.js

// Add validation
const timeOption = document.getElementById('time-option')
timeOption.addEventListener('change', (event) => {
  const val = event.target.value
  if (val < 1 || val > 60) {
    timeOption.value = 25
  }
})

// Save Option
const saveBtn = document.getElementById('save-btn')
saveBtn.addEventListener('click', () => {
  chrome.storage.local.set({
    timeOption: timeOption.value,
    timer: 0,
    isRunning: false,
  })
})

// Load Saved Option
chrome.storage.local.get(['timeOption'], (res) => {
  timeOption.value = res.timeOption
})

В предыдущем коде в option.js:

  • Мы проверяем значения, переданные в качестве параметров. Оно не должно быть меньше 1 и больше 60.
  • Мы сохраняем новую опцию, сбрасываем наш таймер и устанавливаем для параметра isRunning значение false при изменении настройки таймера.

Отображение сохраненной опции во всплывающем окне

Теперь давайте прочитаем сохраненный через фоновый скрипт и отобразим его на всплывающей странице.

In background.js

// create an alarm to notify user when time is up
chrome.alarms.create("pomodoroTimer", {
    periodInMinutes: 1 / 60
})

// alarm listener
chrome.alarms.onAlarm.addListener((alarm) => {
    if (alarm.name === "pomodoroTimer") {
        chrome.storage.local.get(["timer", "isRunning", "timeOption"], (res) => {
            if (res.isRunning) {
                let timer = res.timer + 1
                let isRunning = true
               // console.log(timer)
              if(timer === 60 * res.timeOption) {
                this.registration.showNotification('Pomodoro Timer', {
                   - body: "25 minutes has passed",
                   + body: `${res.timeOption} minutes has passed!`,
                    icon: "icon.png"
                })
                timer = 0
                isRunning = false

               }
                chrome.storage.local.set({
                    timer,
                    isRunning,
                })
            }
        })
    }
})

// storage to set and track timer variables
 chrome.storage.local.get(["timer", "isRunning", "timeOption"], (res) => {
    chrome.storage.local.set({
        timer: "timer" in res ? res.timer : 0,
        timeOption: "timeOption" in res ? res.timeOption : 25,
        isRunning: "isRunning" in res ? res.isRunning : false,
    })
})

In popup.js

// array to store tasks
let tasks = [];

const time = document.getElementById("time");

// Update time every 1sec
function updateTime() {
  chrome.storage.local.get(["timer", "timeOption"], (res) => {
    const time = document.getElementById("time")
    
    // get no. of minutes & secs
    const minutes = `${ res.timeOption - Math.ceil(res.timer / 60)}`.padStart(2, "0");
    let seconds = "00";
    if (res.timer % 60 != 0) {
      seconds = `${60 -res.timer % 60}`.padStart(2, "0");
    }

    // show minutes & secs on UI
  time.textContent = `${minutes}:${seconds}`
  })
}

Если вы протестируете расширение, установив свой параметр, вы увидите новое значение на странице popup.

Стайлинг

Наконец, давайте стилизуем наше расширение. Скопируйте и вставьте код ниже.

In popup.css

body {
  height: 400px;
  width: 350px;
  background: hsla(238, 100%, 71%, 1);

  background: linear-gradient(
    90deg,
    hsla(238, 100%, 71%, 1) 0%,
    hsla(295, 100%, 84%, 1) 100%
  );

  background: -moz-linear-gradient(
    90deg,
    hsla(238, 100%, 71%, 1) 0%,
    hsla(295, 100%, 84%, 1) 100%
  );

  background: -webkit-linear-gradient(
    90deg,
    hsla(238, 100%, 71%, 1) 0%,
    hsla(295, 100%, 84%, 1) 100%
  );

  filter: progid: DXImageTransform.Microsoft.gradient( startColorstr="#696EFF", endColorstr="#F8ACFF", GradientType=1 );
}

.header {
  display: flex;
  justify-content: center;
  height: 40px;
  background-color: whitesmoke;
  margin: -8px;
  padding: 5px;
}

#time {
  text-align: center;
  font-size: 50px;
  margin: 10px;
  font-weight: normal;
  color: whitesmoke;
}

#btn-container {
  display: flex;
  justify-content: space-evenly;
}

#btn-container > button {
  color: black;
  background-color: whitesmoke;
  border: none;
  outline: none;
  border-radius: 5px;
  padding: 8px;
  font-weight: bold;
  width: 100px;
  cursor: pointer;
}

#task-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}

.task-input {
  outline: none;
  border: none;
  border-radius: 4px;
  margin: 5px;
  padding: 5px 10px 5px 10px;
  width: 250px;
}

.task-delete {
  outline: none;
  border: none;
  height: 25px;
  width: 25px;
  border-radius: 4px;
  color: indianred;
  cursor: pointer;
  font-weight: 700;
}

In options.css

body {
  background: hsla(238, 100%, 71%, 1) no-repeat;

  background: linear-gradient(
    90deg,
    hsla(238, 100%, 71%, 1) 0%,
    hsla(295, 100%, 84%, 1) 100%
  ) no-repeat;

  background: -moz-linear-gradient(
    90deg,
    hsla(238, 100%, 71%, 1) 0%,
    hsla(295, 100%, 84%, 1) 100%
  ) no-repeat;

  background: -webkit-linear-gradient(
    90deg,
    hsla(238, 100%, 71%, 1) 0%,
    hsla(295, 100%, 84%, 1) 100%
  ) no-repeat;

  filter: progid: DXImageTransform.Microsoft.gradient( startColorstr="#696EFF", endColorstr="#F8ACFF", GradientType=1 );
}

h1 {
  color: whitesmoke;
  text-align: center;
  font-size: 50px;
  margin: 10px;
  font-weight: normal;
}

h2 {
  font-weight: normal;
  color: whitesmoke;
}

#time-option {
  outline: none;
  border: none;
  width: 300px;
  border-radius: 4px;
  padding: 10px;
}

#save-btn {
  display: block;
  margin-top: 40px;
  border: none;
  outline: none;
  border-radius: 4px;
  padding: 10px;
  color: black;
  font-weight: bold;
  cursor: pointer;
}

Заключение

Мы подошли к концу серии расширений Chrome для начинающих. Я надеюсь, что это помогло вам начать работу с основными концепциями создания расширений Chrome. Я рекомендую вам попробовать создать свои собственные проекты или даже добавить больше функций в этот проект для начала. Всего наилучшего в вашем обучении.

Первоначально опубликовано на blog.openreplay.com 21 декабря 2022 г.