add files

This commit is contained in:
2025-07-07 09:39:03 +08:00
commit c9f6571a0f
181 changed files with 100140 additions and 0 deletions

View File

@ -0,0 +1,29 @@
const commands = {
MG_NAVIGATE: 1,
MG_UPDATE_URI: 2,
MG_GO_FORWARD: 3,
MG_GO_BACK: 4,
MG_NAV_STARTING: 5,
MG_NAV_COMPLETED: 6,
MG_RELOAD: 7,
MG_CANCEL: 8,
MG_CREATE_TAB: 10,
MG_UPDATE_TAB: 11,
MG_SWITCH_TAB: 12,
MG_CLOSE_TAB: 13,
MG_CLOSE_WINDOW: 14,
MG_SHOW_OPTIONS: 15,
MG_HIDE_OPTIONS: 16,
MG_OPTIONS_LOST_FOCUS: 17,
MG_OPTION_SELECTED: 18,
MG_SECURITY_UPDATE: 19,
MG_UPDATE_FAVICON: 20,
MG_GET_SETTINGS: 21,
MG_GET_FAVORITES: 22,
MG_REMOVE_FAVORITE: 23,
MG_CLEAR_CACHE: 24,
MG_CLEAR_COOKIES: 25,
MG_GET_HISTORY: 26,
MG_REMOVE_HISTORY_ITEM: 27,
MG_CLEAR_HISTORY: 28
};

View File

@ -0,0 +1,16 @@
<html>
<head>
<title>Favorites</title>
<link rel="shortcut icon" href="img/favorites.png">
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="items.css">
</head>
<body>
<h1 class="main-title">Favorites</h1>
<div id="entries-container">
You don't have any favorites.
</div>
<script src="../commands.js"></script>
<script src="favorites.js"></script>
<body>
</html>

View File

@ -0,0 +1,95 @@
const messageHandler = event => {
var message = event.data.message;
var args = event.data.args;
switch (message) {
case commands.MG_GET_FAVORITES:
loadFavorites(args.favorites);
break;
default:
console.log(`Unexpected message: ${JSON.stringify(event.data)}`);
break;
}
};
function requestFavorites() {
let message = {
message: commands.MG_GET_FAVORITES,
args: {}
};
window.chrome.webview.postMessage(message);
}
function removeFavorite(uri) {
let message = {
message: commands.MG_REMOVE_FAVORITE,
args: {
uri: uri
}
};
window.chrome.webview.postMessage(message);
}
function loadFavorites(payload) {
let fragment = document.createDocumentFragment();
if (payload.length > 0) {
let container = document.getElementById('entries-container');
container.textContent = '';
}
payload.map(favorite => {
let favoriteContainer = document.createElement('div');
favoriteContainer.className = 'item-container';
let favoriteElement = document.createElement('div');
favoriteElement.className = 'item';
let faviconElement = document.createElement('div');
faviconElement.className = 'favicon';
let faviconImage = document.createElement('img');
faviconImage.src = favorite.favicon;
faviconElement.appendChild(faviconImage);
let labelElement = document.createElement('div');
labelElement.className = 'label-title';
let linkElement = document.createElement('a');
linkElement.textContent = favorite.title;
linkElement.href = favorite.uri;
linkElement.title = favorite.title;
labelElement.appendChild(linkElement);
let uriElement = document.createElement('div');
uriElement.className = 'label-uri';
let textElement = document.createElement('p');
textElement.textContent = favorite.uriToShow || favorite.uri;
textElement.title = favorite.uriToShow || favorite.uri;
uriElement.appendChild(textElement);
let buttonElement = document.createElement('div');
buttonElement.className = 'btn-close';
buttonElement.addEventListener('click', function(e) {
favoriteContainer.parentNode.removeChild(favoriteContainer);
removeFavorite(favorite.uri);
});
favoriteElement.appendChild(faviconElement);
favoriteElement.appendChild(labelElement);
favoriteElement.appendChild(uriElement);
favoriteElement.appendChild(buttonElement);
favoriteContainer.appendChild(favoriteElement);
fragment.appendChild(favoriteContainer);
});
let container = document.getElementById('entries-container');
container.appendChild(fragment);
}
function init() {
window.chrome.webview.addEventListener('message', messageHandler);
requestFavorites();
}
init();

View File

@ -0,0 +1,81 @@
.header-date {
font-weight: 400;
font-size: 14px;
color: rgb(16, 16, 16);
line-height: 20px;
padding-top: 10px;
padding-bottom: 4px;
margin: 0;
}
#btn-clear {
font-size: 14px;
color: rgb(0, 97, 171);
cursor: pointer;
line-height: 20px;
}
#btn-clear.hidden {
display: none;
}
#overlay {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.2);
}
#overlay.hidden {
display: none;
}
#prompt-box {
display: flex;
box-sizing: border-box;
flex-direction: column;
position: fixed;
left: calc(50% - 130px);
top: calc(50% - 70px);
width: 260px;
height: 140px;
padding: 20px;
border-radius: 5px;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.13) 0px 1.6px 20px, rgba(0, 0, 0, 0.11) 0px 0.3px 10px;
}
#prompt-options {
flex: 1;
display: flex;
justify-content: flex-end;
user-select: none;
}
.prompt-btn {
flex: 1;
flex-grow: 0;
align-self: flex-end;
cursor: pointer;
font-family: 'system-ui', sans-serif;
display: inline-block;
padding: 2px 7px;
font-size: 14px;
line-height: 20px;
border-radius: 3px;
font-weight: 400;
}
#prompt-true {
background-color: rgb(0, 112, 198);
color: white;
}
#prompt-false {
background-color: rgb(210, 210, 210);
margin-right: 5px;
}

View File

@ -0,0 +1,30 @@
<html>
<head>
<title>History</title>
<link rel="shortcut icon" href="img/history.png">
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="items.css">
<link rel="stylesheet" type="text/css" href="history.css">
</head>
<body>
<div id="overlay" class="hidden">
<div id="prompt-box">
<span class="prompt-text">Clear history?</span>
<div id="prompt-options">
<div class="prompt-btn" id="prompt-false">Cancel</div>
<div class="prompt-btn" id="prompt-true">Clear</div>
</div>
</div>
</div>
<h1 class="main-title">History</h1>
<div>
<span id="btn-clear" class="hidden">Clear history</span>
</div>
<div id="entries-container">
Loading...
</div>
<script src="../commands.js"></script>
<script src="history.js"></script>
</body>
</html>

View File

@ -0,0 +1,273 @@
const DEFAULT_HISTORY_ITEM_COUNT = 20;
const EMPTY_HISTORY_MESSAGE = `You haven't visited any sites yet.`;
let requestedTop = 0;
let lastRequestSize = 0;
let itemHeight = 48;
const dateStringFormat = new Intl.DateTimeFormat('default', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
const timeStringFormat = new Intl.DateTimeFormat('default', {
hour: '2-digit',
minute: '2-digit'
});
const messageHandler = event => {
var message = event.data.message;
var args = event.data.args;
switch (message) {
case commands.MG_GET_HISTORY:
let entriesContainer = document.getElementById('entries-container');
if (args.from == 0 && args.items.length) {
entriesContainer.textContent = '';
let clearButton = document.getElementById('btn-clear');
clearButton.classList.remove('hidden');
}
loadItems(args.items);
if (args.items.length == lastRequestSize) {
document.addEventListener('scroll', requestTrigger);
} else if (entriesContainer.childElementCount == 0) {
loadUIForEmptyHistory();
}
break;
default:
console.log(`Unexpected message: ${JSON.stringify(event.data)}`);
break;
}
};
const requestTrigger = function(event) {
let triggerRange = 50;
let element = document.body;
if (element.scrollTop + element.clientHeight >= element.scrollHeight - triggerRange) {
getMoreHistoryItems();
event.target.removeEventListener('scroll', requestTrigger);
}
};
function requestHistoryItems(from, count) {
let message = {
message: commands.MG_GET_HISTORY,
args: {
from: from,
count: count || DEFAULT_HISTORY_ITEM_COUNT
}
};
window.chrome.webview.postMessage(message);
}
function removeItem(id) {
let message = {
message: commands.MG_REMOVE_HISTORY_ITEM,
args: {
id: id
}
};
window.chrome.webview.postMessage(message);
}
function createItemElement(item, id, date) {
let itemContainer = document.createElement('div');
itemContainer.id = id;
itemContainer.className = 'item-container';
let itemElement = document.createElement('div');
itemElement.className = 'item';
// Favicon
let faviconElement = document.createElement('div');
faviconElement.className = 'favicon';
let faviconImage = document.createElement('img');
faviconImage.src = item.favicon;
faviconElement.append(faviconImage);
itemElement.append(faviconElement);
// Title
let titleLabel = document.createElement('div');
titleLabel.className = 'label-title';
let linkElement = document.createElement('a');
linkElement.href = item.uri;
linkElement.title = item.title;
linkElement.textContent = item.title;
titleLabel.append(linkElement);
itemElement.append(titleLabel);
// URI
let uriLabel = document.createElement('div');
uriLabel.className = 'label-uri';
let textElement = document.createElement('p');
textElement.title = item.uri;
textElement.textContent = item.uri;
uriLabel.append(textElement);
itemElement.append(uriLabel);
// Time
let timeLabel = document.createElement('div');
timeLabel.className = 'label-time';
let timeText = document.createElement('p');
timeText.textContent = timeStringFormat.format(date);
timeLabel.append(timeText);
itemElement.append(timeLabel);
// Close button
let closeButton = document.createElement('div');
closeButton.className = 'btn-close';
closeButton.addEventListener('click', function(e) {
if (itemContainer.parentNode.children.length <= 2) {
itemContainer.parentNode.remove();
} else {
itemContainer.remove();
}
let entriesContainer = document.getElementById('entries-container');
if (entriesContainer.childElementCount == 0) {
loadUIForEmptyHistory();
}
removeItem(parseInt(id.split('-')[1]));
});
itemElement.append(closeButton);
itemContainer.append(itemElement);
return itemContainer;
}
function createDateContainer(id, date) {
let dateContainer = document.createElement('div');
dateContainer.id = id;
let dateLabel = document.createElement('h3');
dateLabel.className = 'header-date';
dateLabel.textContent = dateStringFormat.format(date);
dateContainer.append(dateLabel);
return dateContainer;
}
function loadItems(items) {
let dateContainer;
let fragment;
items.map((entry) => {
let id = entry.id;
let item = entry.item;
let itemContainerId = `item-${id}`;
// Skip the item if already loaded. This could happen if the user
// visits an item for the current date again before requesting more
// history items.
let itemContainer = document.getElementById(itemContainerId);
if (itemContainer) {
return;
}
let date = new Date(item.timestamp);
let day = date.getDate();
let month = date.getMonth();
let year = date.getFullYear();
let dateContainerId = `entries-${month}-${day}-${year}`;
// If entry belongs to a new date, append buffered items for previous
// date.
if (dateContainer && dateContainer.id != dateContainerId) {
dateContainer.append(fragment);
}
dateContainer = document.getElementById(dateContainerId);
if (!dateContainer) {
dateContainer = createDateContainer(dateContainerId, date);
fragment = document.createDocumentFragment();
let entriesContainer = document.getElementById('entries-container');
entriesContainer.append(dateContainer);
} else if (!fragment) {
fragment = document.createDocumentFragment();
}
itemContainer = createItemElement(item, itemContainerId, date);
fragment.append(itemContainer);
});
// Append remaining items in buffer
if (fragment) {
dateContainer.append(fragment);
}
}
function getMoreHistoryItems(n) {
n = n ? n : DEFAULT_HISTORY_ITEM_COUNT;
requestHistoryItems(requestedTop, n);
requestedTop += n;
lastRequestSize = n;
document.removeEventListener('scroll', requestTrigger);
}
function addUIListeners() {
let confirmButton = document.getElementById('prompt-true');
confirmButton.addEventListener('click', function(event) {
clearHistory();
event.stopPropagation();
});
let cancelButton = document.getElementById('prompt-false');
cancelButton.addEventListener('click', function(event) {
toggleClearPrompt();
event.stopPropagation();
});
let promptBox = document.getElementById('prompt-box');
promptBox.addEventListener('click', function(event) {
event.stopPropagation();
});
let promptOverlay = document.getElementById('overlay');
promptOverlay.addEventListener('click', toggleClearPrompt);
let clearButton = document.getElementById('btn-clear');
clearButton.addEventListener('click', toggleClearPrompt);
}
function toggleClearPrompt() {
let promptOverlay = document.getElementById('overlay');
promptOverlay.classList.toggle('hidden');
}
function loadUIForEmptyHistory() {
let entriesContainer = document.getElementById('entries-container');
entriesContainer.textContent = EMPTY_HISTORY_MESSAGE;
let clearButton = document.getElementById('btn-clear');
clearButton.classList.add('hidden');
}
function clearHistory() {
toggleClearPrompt();
loadUIForEmptyHistory();
let message = {
message: commands.MG_CLEAR_HISTORY,
args: {}
};
window.chrome.webview.postMessage(message);
}
function init() {
window.chrome.webview.addEventListener('message', messageHandler);
let viewportItemsCapacity = Math.round(window.innerHeight / itemHeight);
addUIListeners();
getMoreHistoryItems(viewportItemsCapacity);
}
init();

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,92 @@
.item-container {
box-sizing: border-box;
padding: 4px 0;
height: 48px;
}
.item {
display: flex;
width: 100%;
max-width: 820px;
height: 40px;
width: 100%;
border-radius: 4px;
box-sizing: border-box;
box-shadow: rgba(0, 0, 0, 0.13) 0px 1.6px 3.6px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px;
align-items: center;
background: rgb(255, 255, 255);
}
.item:hover {
box-shadow: 0px 4.8px 10.8px rgba(0,0,0,0.13), 0px 0.9px 2.7px rgba(0,0,0,0.11);
}
.favicon {
display: flex;
height: 40px;
width: 40px;
justify-content: center;
align-items: center;
}
.favicon img {
width: 16px;
height: 16px;
}
.label-title, .label-uri {
display: flex;
flex: 1;
min-width: 0;
height: 40px;
align-items: center;
}
.label-title a {
cursor: pointer;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 600;
font-size: 12px;
line-height: 16px;
color: rgb(16, 16, 16);
border-width: initial;
border-style: none;
border-color: initial;
border-image: initial;
overflow: hidden;
background: none;
text-decoration: none;
}
.label-title a:hover {
text-decoration: underline;
}
.label-uri, .label-time {
margin: 0 6px;
line-height: 16px;
font-size: 12px;
color: rgb(115, 115, 115);
}
.label-uri p, .label-time p {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn-close {
height: 28px;
width: 28px;
margin: 6px;
border-radius: 2px;
background-image: url('img/close.png');
background-size: 100%;
}
.btn-close:hover {
background-color: rgb(243, 243, 243);
}

View File

@ -0,0 +1,54 @@
.settings-entry {
display: block;
height: 48px;
width: 100%;
max-width: 500px;
background: none;
border: none;
padding: 4px 0;
font: inherit;
cursor: pointer;
}
#entry-script, #entry-popups {
display: none;
}
.entry {
display: block;
height: 100%;
text-align: left;
border-radius: 5px;
}
.entry:hover {
background-color: rgb(220, 220, 220);
}
.entry:focus {
outline: none;
}
.entry-name, .entry-value {
display: inline-flex;
height: 100%;
vertical-align: middle;
}
.entry-name span, .entry-value span {
flex: 1;
align-self: center;
}
.entry-name {
padding-left: 10px;
}
.entry-value {
float: right;
vertical-align: middle;
margin: 0 15px;
font-size: 0.8em;
color: gray;
}

View File

@ -0,0 +1,56 @@
<html>
<head>
<title>Settings</title>
<link rel="shortcut icon" href="img/settings.png">
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="settings.css">
</head>
<body>
<h1 class="main-title">Settings</h1>
<div class="page-content">
<button class="settings-entry" id="entry-cache">
<div class="entry">
<div class="entry-name">
<span>Clear cache</span>
</div>
<div class="entry-value">
<span></span>
</div>
</div>
</button>
<button class="settings-entry" id="entry-cookies">
<div class="entry">
<div class="entry-name">
<span>Clear cookies</span>
</div>
<div class="entry-value">
<span></span>
</div>
</div>
</button>
<button class="settings-entry" id="entry-script">
<div class="entry">
<div class="entry-name">
<span>Toggle JavaScript</span>
</div>
<div class="entry-value">
<span></span>
</div>
</div>
</button>
<button class="settings-entry" id="entry-popups">
<div class="entry">
<div class="entry-name">
<span>Block pop-ups</span>
</div>
<div class="entry-value">
<span></span>
</div>
</div>
</button>
</div>
<script src="../commands.js"></script>
<script src="settings.js"></script>
</body>
</html>

View File

@ -0,0 +1,107 @@
const messageHandler = event => {
var message = event.data.message;
var args = event.data.args;
switch (message) {
case commands.MG_GET_SETTINGS:
loadSettings(args.settings);
break;
case commands.MG_CLEAR_CACHE:
if (args.content && args.controls) {
updateLabelForEntry('entry-cache', 'Cleared');
} else {
updateLabelForEntry('entry-cache', 'Try again');
}
break;
case commands.MG_CLEAR_COOKIES:
if (args.content && args.controls) {
updateLabelForEntry('entry-cookies', 'Cleared');
} else {
updateLabelForEntry('entry-cookies', 'Try again');
}
break;
default:
console.log(`Unexpected message: ${JSON.stringify(event.data)}`);
break;
}
};
function addEntriesListeners() {
let cacheEntry = document.getElementById('entry-cache');
cacheEntry.addEventListener('click', function(e) {
let message = {
message: commands.MG_CLEAR_CACHE,
args: {}
};
window.chrome.webview.postMessage(message);
});
let cookiesEntry = document.getElementById('entry-cookies');
cookiesEntry.addEventListener('click', function(e) {
let message = {
message: commands.MG_CLEAR_COOKIES,
args: {}
};
window.chrome.webview.postMessage(message);
});
let scriptEntry = document.getElementById('entry-script');
scriptEntry.addEventListener('click', function(e) {
// Toggle script support
});
let popupsEntry = document.getElementById('entry-popups');
popupsEntry.addEventListener('click', function(e) {
// Toggle popups
});
}
function requestBrowserSettings() {
let message = {
message: commands.MG_GET_SETTINGS,
args: {}
};
window.chrome.webview.postMessage(message);
}
function loadSettings(settings) {
if (settings.scriptsEnabled) {
updateLabelForEntry('entry-script', 'Enabled');
} else {
updateLabelForEntry('entry-script', 'Disabled');
}
if (settings.blockPopups) {
updateLabelForEntry('entry-popups', 'Blocked');
} else {
updateLabelForEntry('entry-popups', 'Allowed');
}
}
function updateLabelForEntry(elementId, label) {
let entryElement = document.getElementById(elementId);
if (!entryElement) {
console.log(`Element with id ${elementId} does not exist`);
return;
}
let labelSpan = entryElement.querySelector(`.entry-value span`);
if (!labelSpan) {
console.log(`${elementId} does not have a label`);
return;
}
labelSpan.textContent = label;
}
function init() {
window.chrome.webview.addEventListener('message', messageHandler);
requestBrowserSettings();
addEntriesListeners();
}
init();

View File

@ -0,0 +1,25 @@
html, body {
margin: 0;
border: none;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
background-color: rgb(240, 240, 242);
}
p {
margin: 0;
}
body {
padding: 32px 50px;
font-family: 'system-ui', sans-serif;
}
.main-title {
display: block;
margin-bottom: 18px;
font-size: 24px;
font-weight: 600;
color: rgb(16, 16, 16);
}

View File

@ -0,0 +1,135 @@
#address-bar-container {
display: flex;
height: calc(100% - 10px);
width: 80%;
max-width: calc(100% - 160px);
background-color: white;
border: 1px solid gray;
border-radius: 5px;
position: relative;
align-self: center;
}
#address-bar-container:focus-within {
outline: none;
box-shadow: 0 0 3px dodgerblue;
}
#address-bar-container:focus-within #btn-clear {
display: block;
}
#security-label {
display: inline-flex;
height: 100%;
margin-left: 2px;
vertical-align: top;
}
#security-label span {
font-family: Arial;
font-size: 0.9em;
color: gray;
vertical-align: middle;
flex: 1;
align-self: center;
text-align: left;
padding-left: 5px;
white-space: nowrap;
}
.icn {
display: inline-block;
margin: 2px 0;
border-radius: 5px;
top: 0;
width: 26px;
height: 26px;
}
#icn-lock {
background-size: 100%;
}
#security-label.label-unknown .icn {
background-image: url('img/unknown.png');
}
#security-label.label-insecure .icn {
background-image: url('img/insecure.png');
}
#security-label.label-insecure span {
color: rgb(192, 0, 0);
}
#security-label.label-neutral .icn {
background-image: url('img/neutral.png');
}
#security-label.label-secure .icn {
background-image: url('img/secure.png');
}
#security-label.label-secure span, #security-label.label-neutral span {
display: none;
}
#icn-favicon {
background-size: 100%;
}
#img-favicon {
width: 18px;
height: 18px;
padding: 4px;
}
#address-form {
margin: 0;
}
#address-field {
flex: 1;
padding: 0;
border: none;
border-radius: 5px;
margin: 0;
line-height: 30px;
width: 100%;
}
#address-field:focus {
outline: none;
}
#btn-fav {
margin: 2px 5px;
background-size: 100%;
background-image: url('img/favorite.png');
}
#btn-fav:hover, #btn-clear:hover {
background-color: rgb(230, 230, 230);
}
#btn-fav.favorited {
background-image: url('img/favorited.png');
}
#btn-clear {
display: none;
width: 16px;
height: 16px;
border: none;
align-self: center;
background-color: transparent;
background-image: url(img/cancel.png);
background-size: 100%;
border: none;
border-radius: 8px;
}

View File

@ -0,0 +1,66 @@
#controls-bar {
display: flex;
justify-content: space-between;
flex-direction: row;
height: 40px;
background-color: rgb(230, 230, 230);
}
.btn, .btn-disabled, .btn-cancel, .btn-active {
display: inline-block;
border: none;
margin: 5px 0;
border-radius: 5px;
outline: none;
height: 30px;
width: 30px;
background-size: 100%;
}
#btn-forward {
background-image: url('img/goForward.png');
}
.btn-disabled#btn-forward {
background-image: url('img/goForward_disabled.png');
}
#btn-back {
background-image: url('img/goBack.png');
}
.btn-disabled#btn-back {
background-image: url('img/goBack_disabled.png');
}
#btn-reload {
background-image: url('img/reload.png');
}
.btn-cancel#btn-reload {
background-image: url('img/cancel.png');
}
#btn-options {
background-image: url('img/options.png');
}
.controls-group {
display: inline-block;
height: 40px;
}
#nav-controls-container {
align-self: flex-start;
padding-left: 10px;
}
#manage-controls-container {
align-self: flex-end;
padding-right: 10px;
}
.btn:hover, .btn-cancel:hover, .btn-active {
background-color: rgb(200, 200, 200);
}

View File

@ -0,0 +1,3 @@
html, body {
height: 70px;
}

View File

@ -0,0 +1,17 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="default.css">
<link rel="stylesheet" type="text/css" href="controls.css">
<link rel="stylesheet" type="text/css" href="address-bar.css">
<link rel="stylesheet" type="text/css" href="strip.css">
</head>
<body>
<script src="../commands.js"></script>
<script src="tabs.js"></script>
<script src="storage.js"></script>
<script src="favorites.js"></script>
<script src="history.js"></script>
<script src="default.js"></script>
</body>
</html>

View File

@ -0,0 +1,736 @@
const WORD_REGEX = /^[^//][^.]*$/;
const VALID_URI_REGEX = /^[-:.&#+()[\]$'*;@~!,?%=\/\w]+$/; // Will check that only RFC3986 allowed characters are included
const SCHEMED_URI_REGEX = /^\w+:.+$/;
let settings = {
scriptsEnabled: true,
blockPopups: true
};
const messageHandler = event => {
var message = event.data.message;
var args = event.data.args;
switch (message) {
case commands.MG_UPDATE_URI:
if (isValidTabId(args.tabId)) {
const tab = tabs.get(args.tabId);
let previousURI = tab.uri;
// Update the tab state
tab.uri = args.uri;
tab.uriToShow = args.uriToShow;
tab.canGoBack = args.canGoBack;
tab.canGoForward = args.canGoForward;
// If the tab is active, update the controls UI
if (args.tabId == activeTabId) {
updateNavigationUI(message);
}
isFavorite(tab.uri, (isFavorite) => {
tab.isFavorite = isFavorite;
updateFavoriteIcon();
});
// Don't add history entry if URI has not changed
if (tab.uri == previousURI) {
break;
}
// Filter URIs that should not appear in history
if (!tab.uri || tab.uri == 'about:blank') {
tab.historyItemId = INVALID_HISTORY_ID;
break;
}
if (tab.uriToShow && tab.uriToShow.substring(0, 10) == 'browser://') {
tab.historyItemId = INVALID_HISTORY_ID;
break;
}
addHistoryItem(historyItemFromTab(args.tabId), (id) => {
tab.historyItemId = id;
});
}
break;
case commands.MG_NAV_STARTING:
if (isValidTabId(args.tabId)) {
// Update the tab state
tabs.get(args.tabId).isLoading = true;
// If the tab is active, update the controls UI
if (args.tabId == activeTabId) {
updateNavigationUI(message);
}
}
break;
case commands.MG_NAV_COMPLETED:
if (isValidTabId(args.tabId)) {
// Update tab state
tabs.get(args.tabId).isLoading = false;
// If the tab is active, update the controls UI
if (args.tabId == activeTabId) {
updateNavigationUI(message);
}
}
break;
case commands.MG_UPDATE_TAB:
if (isValidTabId(args.tabId)) {
const tab = tabs.get(args.tabId);
const tabElement = document.getElementById(`tab-${args.tabId}`);
if (!tabElement) {
refreshTabs();
return;
}
// Update tab label
// Use given title or fall back to a generic tab title
tab.title = args.title || 'Tab';
const tabLabel = tabElement.firstChild;
const tabLabelSpan = tabLabel.firstChild;
tabLabelSpan.textContent = tab.title;
// Update title in history item
// Browser pages will keep an invalid history ID
if (tab.historyItemId != INVALID_HISTORY_ID) {
updateHistoryItem(tab.historyItemId, historyItemFromTab(args.tabId));
}
}
break;
case commands.MG_OPTIONS_LOST_FOCUS:
let optionsButton = document.getElementById('btn-options');
if (optionsButton) {
if (optionsButton.className = 'btn-active') {
toggleOptionsDropdown();
}
}
break;
case commands.MG_SECURITY_UPDATE:
if (isValidTabId(args.tabId)) {
const tab = tabs.get(args.tabId);
tab.securityState = args.state;
if (args.tabId == activeTabId) {
updateNavigationUI(message);
}
}
break;
case commands.MG_UPDATE_FAVICON:
if (isValidTabId(args.tabId)) {
updateFaviconURI(args.tabId, args.uri);
}
break;
case commands.MG_CLOSE_WINDOW:
closeWindow();
break;
case commands.MG_CLOSE_TAB:
if (isValidTabId(args.tabId)) {
closeTab(args.tabId);
}
break;
case commands.MG_GET_FAVORITES:
if (isValidTabId(args.tabId)) {
getFavoritesAsJson((payload) => {
args.favorites = payload;
window.chrome.webview.postMessage(event.data);
});
}
break;
case commands.MG_REMOVE_FAVORITE:
removeFavorite(args.uri);
break;
case commands.MG_GET_SETTINGS:
if (isValidTabId(args.tabId)) {
args.settings = settings;
window.chrome.webview.postMessage(event.data);
}
break;
case commands.MG_GET_HISTORY:
if (isValidTabId(args.tabId)) {
getHistoryItems(args.from, args.count, (payload) => {
args.items = payload;
window.chrome.webview.postMessage(event.data);
});
}
break;
case commands.MG_REMOVE_HISTORY_ITEM:
removeHistoryItem(args.id);
break;
case commands.MG_CLEAR_HISTORY:
clearHistory();
break;
default:
console.log(`Received unexpected message: ${JSON.stringify(event.data)}`);
}
};
function processAddressBarInput() {
var text = document.querySelector('#address-field').value;
tryNavigate(text);
}
function tryNavigate(text) {
try {
var uriParser = new URL(text);
// URL creation succeeded, verify protocol is allowed
switch (uriParser.protocol) {
case 'http:':
case 'https:':
case 'file:':
case 'ftp:':
// Allowed protocol, navigate
navigateActiveTab(uriParser.href, false);
break;
default:
// Protocol not allowed, search Bing
navigateActiveTab(getSearchURI(text), true);
break;
}
} catch (e) {
// URL creation failed, check for invalid characters
if (containsIlegalCharacters(text) || isSingleWord(text)) {
// Search Bing
navigateActiveTab(getSearchURI(text), true);
} else {
// Try with HTTP
if (!hasScheme(text)) {
tryNavigate(`http:${text}`);
} else {
navigateActiveTab(getSearchURI(text), true);
}
}
}
}
function navigateActiveTab(uri, isSearch) {
var message = {
message: commands.MG_NAVIGATE,
args: {
uri: uri,
encodedSearchURI: isSearch ? uri : getSearchURI(uri)
}
};
window.chrome.webview.postMessage(message);
}
function reloadActiveTabContent() {
var message = {
message: commands.MG_RELOAD,
args: {}
};
window.chrome.webview.postMessage(message);
}
function containsIlegalCharacters(query) {
return !VALID_URI_REGEX.test(query);
}
function isSingleWord(query) {
return WORD_REGEX.test(query);
}
function hasScheme(query) {
return SCHEMED_URI_REGEX.test(query);
}
function getSearchURI(query) {
return `https://www.bing.com/search?q=${encodeURIComponent(query)}`;
}
function closeWindow() {
var message = {
message: commands.MG_CLOSE_WINDOW,
args: {}
};
window.chrome.webview.postMessage(message);
}
// Show active tab's URI in the address bar
function updateURI() {
if (activeTabId == INVALID_TAB_ID) {
return;
}
let activeTab = tabs.get(activeTabId);
document.getElementById('address-field').value = activeTab.uriToShow || activeTab.uri;
}
// Show active tab's favicon in the address bar
function updateFavicon() {
if (activeTabId == INVALID_TAB_ID) {
return;
}
let activeTab = tabs.get(activeTabId);
let faviconElement = document.getElementById('img-favicon');
if (!faviconElement) {
refreshControls();
return;
}
faviconElement.src = activeTab.favicon;
}
// Update back and forward buttons for the active tab
function updateBackForwardButtons() {
if (activeTabId == INVALID_TAB_ID) {
return;
}
let activeTab = tabs.get(activeTabId);
let btnForward = document.getElementById('btn-forward');
let btnBack = document.getElementById('btn-back');
if (!btnBack || !btnForward) {
refreshControls();
return;
}
if (activeTab.canGoForward)
btnForward.className = 'btn';
else
btnForward.className = 'btn-disabled';
if (activeTab.canGoBack)
btnBack.className = 'btn';
else
btnBack.className = 'btn-disabled';
}
// Update reload button for the active tab
function updateReloadButton() {
if (activeTabId == INVALID_TAB_ID) {
return;
}
let activeTab = tabs.get(activeTabId);
let btnReload = document.getElementById('btn-reload');
if (!btnReload) {
refreshControls();
return;
}
btnReload.className = activeTab.isLoading ? 'btn-cancel' : 'btn';
}
// Update lock icon for the active tab
function updateLockIcon() {
if (activeTabId == INVALID_TAB_ID) {
return;
}
let activeTab = tabs.get(activeTabId);
let labelElement = document.getElementById('security-label');
if (!labelElement) {
refreshControls();
return;
}
switch (activeTab.securityState) {
case 'insecure':
labelElement.className = 'label-insecure';
break;
case 'neutral':
labelElement.className = 'label-neutral';
break;
case 'secure':
labelElement.className = 'label-secure';
break;
default:
labelElement.className = 'label-unknown';
break;
}
}
// Update favorite status for the active tab
function updateFavoriteIcon() {
if (activeTabId == INVALID_TAB_ID) {
return;
}
let activeTab = tabs.get(activeTabId);
isFavorite(activeTab.uri, (isFavorite) => {
let favoriteElement = document.getElementById('btn-fav');
if (!favoriteElement) {
refreshControls();
return;
}
if (isFavorite) {
favoriteElement.classList.add('favorited');
activeTab.isFavorite = true;
} else {
favoriteElement.classList.remove('favorited');
activeTab.isFavorite = false;
}
});
}
function updateNavigationUI(reason) {
switch (reason) {
case commands.MG_UPDATE_URI:
updateURI();
updateFavoriteIcon();
updateBackForwardButtons();
break;
case commands.MG_NAV_COMPLETED:
case commands.MG_NAV_STARTING:
updateReloadButton();
break;
case commands.MG_SECURITY_UPDATE:
updateLockIcon();
break;
case commands.MG_UPDATE_FAVICON:
updateFavicon();
break;
// If a reason is not provided (for requests not originating from a
// message), default to switch tab behavior.
default:
case commands.MG_SWITCH_TAB:
updateURI();
updateLockIcon();
updateFavicon();
updateFavoriteIcon();
updateReloadButton();
updateBackForwardButtons();
break;
}
}
function loadTabUI(tabId) {
if (isValidTabId(tabId)) {
let tab = tabs.get(tabId);
let tabElement = document.createElement('div');
tabElement.className = tabId == activeTabId ? 'tab-active' : 'tab';
tabElement.id = `tab-${tabId}`;
let tabLabel = document.createElement('div');
tabLabel.className = 'tab-label';
let labelText = document.createElement('span');
labelText.textContent = tab.title;
tabLabel.appendChild(labelText);
let closeButton = document.createElement('div');
closeButton.className = 'btn-tab-close';
closeButton.addEventListener('click', function(e) {
closeTab(tabId);
});
tabElement.appendChild(tabLabel);
tabElement.appendChild(closeButton);
var createTabButton = document.getElementById('btn-new-tab');
document.getElementById('tabs-strip').insertBefore(tabElement, createTabButton);
tabElement.addEventListener('click', function(e) {
if (e.srcElement.className != 'btn-tab-close') {
switchToTab(tabId, true);
}
});
}
}
function toggleOptionsDropdown() {
const optionsButtonElement = document.getElementById('btn-options');
const elementClass = optionsButtonElement.className;
var message;
if (elementClass === 'btn') {
// Update UI
optionsButtonElement.className = 'btn-active';
message = {
message: commands.MG_SHOW_OPTIONS,
args: {}
};
} else {
// Update UI
optionsButtonElement.className = 'btn';
message = {
message:commands.MG_HIDE_OPTIONS,
args: {}
};
}
window.chrome.webview.postMessage(message);
}
function refreshControls() {
let controlsElement = document.getElementById('controls-bar');
if (controlsElement) {
controlsElement.remove();
}
controlsElement = document.createElement('div');
controlsElement.id = 'controls-bar';
// Navigation controls
let navControls = document.createElement('div');
navControls.className = 'controls-group';
navControls.id = 'nav-controls-container';
let backButton = document.createElement('div');
backButton.className = 'btn-disabled';
backButton.id = 'btn-back';
navControls.append(backButton);
let forwardButton = document.createElement('div');
forwardButton.className = 'btn-disabled';
forwardButton.id = 'btn-forward';
navControls.append(forwardButton);
let reloadButton = document.createElement('div');
reloadButton.className = 'btn';
reloadButton.id = 'btn-reload';
navControls.append(reloadButton);
controlsElement.append(navControls);
// Address bar
let addressBar = document.createElement('div');
addressBar.id = 'address-bar-container';
let securityLabel = document.createElement('div');
securityLabel.className = 'label-unknown';
securityLabel.id = 'security-label';
let labelSpan = document.createElement('span');
labelSpan.textContent = 'Not secure';
securityLabel.append(labelSpan);
let lockIcon = document.createElement('div');
lockIcon.className = 'icn';
lockIcon.id = 'icn-lock';
securityLabel.append(lockIcon);
addressBar.append(securityLabel);
let faviconElement = document.createElement('div');
faviconElement.className = 'icn';
faviconElement.id = 'icn-favicon';
let faviconImage = document.createElement('img');
faviconImage.id = 'img-favicon';
faviconImage.src = 'img/favicon.png';
faviconElement.append(faviconImage);
addressBar.append(faviconElement);
let addressInput = document.createElement('input');
addressInput.id = 'address-field';
addressInput.placeholder = 'Search or enter web address';
addressInput.type = 'text';
addressInput.spellcheck = false;
addressBar.append(addressInput);
let clearButton = document.createElement('button');
clearButton.id = 'btn-clear';
addressBar.append(clearButton);
let favoriteButton = document.createElement('div');
favoriteButton.className = 'icn';
favoriteButton.id = 'btn-fav';
addressBar.append(favoriteButton);
controlsElement.append(addressBar);
// Manage controls
let manageControls = document.createElement('div');
manageControls.className = 'controls-group';
manageControls.id = 'manage-controls-container';
let optionsButton = document.createElement('div');
optionsButton.className = 'btn';
optionsButton.id = 'btn-options';
manageControls.append(optionsButton);
controlsElement.append(manageControls);
// Insert controls bar into document
let tabsElement = document.getElementById('tabs-strip');
if (tabsElement) {
tabsElement.parentElement.insertBefore(controlsElement, tabsElement);
} else {
let bodyElement = document.getElementsByTagName('body')[0];
bodyElement.append(controlsElement);
}
addControlsListeners();
updateNavigationUI();
}
function refreshTabs() {
let tabsStrip = document.getElementById('tabs-strip');
if (tabsStrip) {
tabsStrip.remove();
}
tabsStrip = document.createElement('div');
tabsStrip.id = 'tabs-strip';
let newTabButton = document.createElement('div');
newTabButton.id = 'btn-new-tab';
let buttonSpan = document.createElement('span');
buttonSpan.textContent = '+';
buttonSpan.id = 'plus-label';
newTabButton.append(buttonSpan);
tabsStrip.append(newTabButton);
let bodyElement = document.getElementsByTagName('body')[0];
bodyElement.append(tabsStrip);
addTabsListeners();
Array.from(tabs).map((tabEntry) => {
loadTabUI(tabEntry[0]);
});
}
function toggleFavorite() {
activeTab = tabs.get(activeTabId);
if (activeTab.isFavorite) {
removeFavorite(activeTab.uri, () => {
activeTab.isFavorite = false;
updateFavoriteIcon();
});
} else {
addFavorite(favoriteFromTab(activeTabId), () => {
activeTab.isFavorite = true;
updateFavoriteIcon();
});
}
}
function addControlsListeners() {
let inputField = document.querySelector('#address-field');
let clearButton = document.querySelector('#btn-clear');
inputField.addEventListener('keypress', function(e) {
var key = e.which || e.keyCode;
if (key === 13) { // 13 is enter
e.preventDefault();
processAddressBarInput();
}
});
inputField.addEventListener('focus', function(e) {
e.target.select();
});
inputField.addEventListener('blur', function(e) {
inputField.setSelectionRange(0, 0);
if (!inputField.value) {
updateURI();
}
});
clearButton.addEventListener('click', function(e) {
inputField.value = '';
inputField.focus();
e.preventDefault();
});
document.querySelector('#btn-forward').addEventListener('click', function(e) {
if (document.getElementById('btn-forward').className === 'btn') {
var message = {
message: commands.MG_GO_FORWARD,
args: {}
};
window.chrome.webview.postMessage(message);
}
});
document.querySelector('#btn-back').addEventListener('click', function(e) {
if (document.getElementById('btn-back').className === 'btn') {
var message = {
message: commands.MG_GO_BACK,
args: {}
};
window.chrome.webview.postMessage(message);
}
});
document.querySelector('#btn-reload').addEventListener('click', function(e) {
var btnReload = document.getElementById('btn-reload');
if (btnReload.className === 'btn-cancel') {
var message = {
message: commands.MG_CANCEL,
args: {}
};
window.chrome.webview.postMessage(message);
} else if (btnReload.className === 'btn') {
reloadActiveTabContent();
}
});
document.querySelector('#btn-options').addEventListener('click', function(e) {
toggleOptionsDropdown();
});
window.onkeydown = function(event) {
if (event.ctrlKey) {
switch (event.key) {
case 'r':
case 'R':
reloadActiveTabContent();
break;
case 'd':
case 'D':
toggleFavorite();
break;
case 't':
case 'T':
createNewTab(true);
break;
case 'p':
case 'P':
case '+':
case '-':
case '_':
case '=':
break;
default:
return;
}
event.preventDefault();
}
};
// Prevent zooming the UI
window.addEventListener('wheel', function(event) {
if (event.ctrlKey) {
event.preventDefault();
}}, { passive: false });
}
function addTabsListeners() {
document.querySelector('#btn-new-tab').addEventListener('click', function(e) {
createNewTab(true);
});
document.querySelector('#btn-fav').addEventListener('click', function(e) {
toggleFavorite();
});
}
function init() {
window.chrome.webview.addEventListener('message', messageHandler);
refreshControls();
refreshTabs();
createNewTab(true);
}
init();

View File

@ -0,0 +1,72 @@
function isFavorite(uri, callback) {
queryDB((db) => {
let transaction = db.transaction(['favorites']);
let favoritesStore = transaction.objectStore('favorites');
let favoriteStatusRequest = favoritesStore.get(uri);
favoriteStatusRequest.onerror = function(event) {
console.log(`Could not query for ${uri}: ${event.target.error.message}`);
};
favoriteStatusRequest.onsuccess = function() {
callback(favoriteStatusRequest.result);
};
});
}
function addFavorite(favorite, callback) {
queryDB((db) => {
let transaction = db.transaction(['favorites'], 'readwrite');
let favoritesStore = transaction.objectStore('favorites');
let addFavoriteRequest = favoritesStore.add(favorite);
addFavoriteRequest.onerror = function(event) {
console.log(`Could not add favorite with key: ${favorite.uri}`);
console.log(event.target.error.message);
};
addFavoriteRequest.onsuccess = function(event) {
if (callback) {
callback();
}
};
});
}
function removeFavorite(key, callback) {
queryDB((db) => {
let transaction = db.transaction(['favorites'], 'readwrite');
let favoritesStore = transaction.objectStore('favorites');
let removeFavoriteRequest = favoritesStore.delete(key);
removeFavoriteRequest.onerror = function(event) {
console.log(`Could not remove favorite with key: ${key}`);
console.log(event.target.error.message);
};
removeFavoriteRequest.onsuccess = function(event) {
if (callback) {
callback();
}
};
});
}
function getFavoritesAsJson(callback) {
queryDB((db) => {
let transaction = db.transaction(['favorites']);
let favoritesStore = transaction.objectStore('favorites');
let getFavoritesRequest = favoritesStore.getAll();
getFavoritesRequest.onerror = function(event) {
console.log(`Could retrieve favorites`);
console.log(event.target.error.message);
};
getFavoritesRequest.onsuccess = function(event) {
if (callback) {
callback(getFavoritesRequest.result);
}
};
});
}

View File

@ -0,0 +1,143 @@
const INVALID_HISTORY_ID = -1;
function addHistoryItem(item, callback) {
queryDB((db) => {
let transaction = db.transaction(['history'], 'readwrite');
let historyStore = transaction.objectStore('history');
// Check if an item for this URI exists on this day
let currentDate = new Date();
let year = currentDate.getFullYear();
let month = currentDate.getMonth();
let date = currentDate.getDate();
let todayDate = new Date(year, month, date);
let existingItemsIndex = historyStore.index('stampedURI');
let lowerBound = [item.uri, todayDate];
let upperBound = [item.uri, currentDate];
let range = IDBKeyRange.bound(lowerBound, upperBound);
let request = existingItemsIndex.openCursor(range);
request.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
// There's an entry for this URI, update the item
cursor.value.timestamp = item.timestamp;
let updateRequest = cursor.update(cursor.value);
updateRequest.onsuccess = function(event) {
if (callback) {
callback(event.target.result.primaryKey);
}
};
} else {
// No entry for this URI, add item
let addItemRequest = historyStore.add(item);
addItemRequest.onsuccess = function(event) {
if (callback) {
callback(event.target.result);
}
};
}
};
});
}
function updateHistoryItem(id, item, callback) {
if (!id) {
return;
}
queryDB((db) => {
let transaction = db.transaction(['history'], 'readwrite');
let historyStore = transaction.objectStore('history');
let storedItemRequest = historyStore.get(id);
storedItemRequest.onsuccess = function(event) {
let storedItem = event.target.result;
item.timestamp = storedItem.timestamp;
let updateRequest = historyStore.put(item, id);
updateRequest.onsuccess = function(event) {
if (callback) {
callback();
}
};
};
});
}
function getHistoryItems(from, n, callback) {
if (n <= 0 || from < 0) {
if (callback) {
callback([]);
}
}
queryDB((db) => {
let transaction = db.transaction(['history']);
let historyStore = transaction.objectStore('history');
let timestampIndex = historyStore.index('timestamp');
let cursorRequest = timestampIndex.openCursor(null, 'prev');
let current = 0;
let items = [];
cursorRequest.onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor || current >= from + n) {
if (callback) {
callback(items);
}
return;
}
if (current >= from) {
items.push({
id: cursor.primaryKey,
item: cursor.value
});
}
++current;
cursor.continue();
};
});
}
function removeHistoryItem(id, callback) {
queryDB((db) => {
let transaction = db.transaction(['history'], 'readwrite');
let historyStore = transaction.objectStore('history');
let removeItemRequest = historyStore.delete(id);
removeItemRequest.onerror = function(event) {
console.log(`Could not remove history item with ID: ${id}`);
console.log(event.target.error.message);
};
removeItemRequest.onsuccess = function(event) {
if (callback) {
callback();
}
};
});
}
function clearHistory(callback) {
queryDB((db) => {
let transaction = db.transaction(['history'], 'readwrite');
let historyStore = transaction.objectStore('history');
let clearRequest = historyStore.clear();
clearRequest.onsuccess = function(event) {
if (callback) {
callback();
}
};
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,38 @@
html, body {
width: 200px;
height: 107px;
}
#dropdown-wrapper {
width: calc(100% - 2px);
height: calc(100% - 2px);
border: 1px solid gray;
}
.dropdown-item {
background-color: rgb(240, 240, 240);
}
.dropdown-item:hover {
background-color: rgb(220, 220, 220);
}
.item-label {
display: flex;
height: 35px;
text-align: center;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
}
.item-label span {
font-family: Arial;
font-size: 0.8em;
vertical-align: middle;
flex: 1;
align-self: center;
text-align: left;
padding-left: 10px;
}

View File

@ -0,0 +1,28 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="options.css">
</head>
<body>
<div id="dropdown-wrapper">
<div id="item-settings" class="dropdown-item">
<div class="item-label">
<span>Settings</span>
</div>
</div>
<div id="item-history" class="dropdown-item">
<div class="item-label">
<span>History</span>
</div>
</div>
<div id="item-favorites" class="dropdown-item">
<div class="item-label">
<span>Favorites</span>
</div>
</div>
</div>
<script src="../commands.js"></script>
<script src="options.js"></script>
</body>
</html>

View File

@ -0,0 +1,46 @@
function navigateToBrowserPage(path) {
const navMessage = {
message: commands.MG_NAVIGATE,
args: {
uri: `browser://${path}`
}
};
window.chrome.webview.postMessage(navMessage);
}
// Add listener for the options menu entries
function addItemsListeners() {
// Close dropdown when item is selected
(() => {
const dropdownItems = Array.from(document.getElementsByClassName('dropdown-item'));
dropdownItems.map(item => {
item.addEventListener('click', function(e) {
const closeMessage = {
message: commands.MG_OPTION_SELECTED,
args: {}
};
window.chrome.webview.postMessage(closeMessage);
});
// Navigate to browser page
let entry = item.id.split('-')[1];
switch (entry) {
case 'settings':
case 'history':
case 'favorites':
item.addEventListener('click', function(e) {
navigateToBrowserPage(entry);
});
break;
}
});
})();
}
function init() {
addItemsListeners();
}
init();

View File

@ -0,0 +1,44 @@
function handleUpgradeEvent(event) {
console.log('Creating DB');
let newDB = event.target.result;
newDB.onerror = function(event) {
console.log('Something went wrong');
console.log(event);
};
let newFavoritesStore = newDB.createObjectStore('favorites', {
keyPath: 'uri'
});
newFavoritesStore.transaction.oncomplete = function(event) {
console.log('Object stores created');
};
let newHistoryStore = newDB.createObjectStore('history', {
autoIncrement: true
});
newHistoryStore.createIndex('timestamp', 'timestamp', {
unique: false
});
newHistoryStore.createIndex('stampedURI', ['uri', 'timestamp'], {
unique: false
});
}
function queryDB(query) {
let request = window.indexedDB.open('WVBrowser');
request.onerror = function(event) {
console.log('Failed to open database');
};
request.onsuccess = function(event) {
let db = event.target.result;
query(db);
};
request.onupgradeneeded = handleUpgradeEvent;
}

View File

@ -0,0 +1,72 @@
#tabs-strip {
display: flex;
height: 28px;
border-top: 1px solid rgb(200, 200, 200);
border-bottom: 1px solid rgb(200, 200, 200);
background-color: rgb(230, 230, 230);
}
.tab, .tab-active {
display: flex;
flex: 1;
height: 100%;
border-right: 1px solid rgb(200, 200, 200);
overflow: hidden;
max-width: 250px;
}
.tab-active {
background-color: rgb(240, 240, 240);
}
#btn-new-tab {
display: flex;
height: 100%;
width: 30px;
}
#btn-new-tab:hover, .btn-tab-close:hover {
background-color: rgb(200, 200, 200);
}
#plus-label {
display: block;
flex: 1;
align-self: center;
line-height: 30px;
width: 30px;
text-align: center;
}
.btn-tab-close {
display: inline-block;
align-self: center;
min-width: 16px;
width: 16px;
height: 16px;
margin: 0 5px;
border-radius: 4px;
background-size: 100%;
background-image: url('img/cancel.png');
}
.tab-label {
display: flex;
flex: 1;
padding: 5px;
padding-left: 10px;
height: calc(100% - 10px);
vertical-align: middle;
white-space:nowrap;
overflow:hidden;
}
.tab-label span {
font-family: Arial;
font-size: 0.8em;
flex: 1;
align-self: center;
}

View File

@ -0,0 +1,13 @@
html, body {
margin: 0;
border: none;
padding: 0;
}
p {
margin: 0;
}
* {
user-select: none;
}

View File

@ -0,0 +1,216 @@
var tabs = new Map();
var tabIdCounter = 0;
var activeTabId = 0;
const INVALID_TAB_ID = 0;
function getNewTabId() {
return ++tabIdCounter;
}
function isValidTabId(tabId) {
return tabId != INVALID_TAB_ID && tabs.has(tabId);
}
function createNewTab(shouldBeActive) {
const tabId = getNewTabId();
var message = {
message: commands.MG_CREATE_TAB,
args: {
tabId: parseInt(tabId),
active: shouldBeActive || false
}
};
window.chrome.webview.postMessage(message);
tabs.set(parseInt(tabId), {
title: 'New Tab',
uri: '',
uriToShow: '',
favicon: 'img/favicon.png',
isFavorite: false,
isLoading: false,
canGoBack: false,
canGoForward: false,
securityState: 'unknown',
historyItemId: INVALID_HISTORY_ID
});
loadTabUI(tabId);
if (shouldBeActive) {
switchToTab(tabId, false);
}
}
function switchToTab(id, updateOnHost) {
if (!id) {
console.log('ID not provided');
return;
}
// Check the tab to switch to is valid
if (!isValidTabId(id)) {
return;
}
// No need to switch if the tab is already active
if (id == activeTabId) {
return;
}
// Get the tab element to switch to
var tab = document.getElementById(`tab-${id}`);
if (!tab) {
console.log(`Can't switch to tab ${id}: element does not exist`);
return;
}
// Change the style for the previously active tab
if (isValidTabId(activeTabId)) {
const activeTabElement = document.getElementById(`tab-${activeTabId}`);
// Check the previously active tab element does actually exist
if (activeTabElement) {
activeTabElement.className = 'tab';
}
}
// Set tab as active
tab.className = 'tab-active';
activeTabId = id;
// Instruct host app to switch tab
if (updateOnHost) {
var message = {
message: commands.MG_SWITCH_TAB,
args: {
tabId: parseInt(activeTabId)
}
};
window.chrome.webview.postMessage(message);
}
updateNavigationUI(commands.MG_SWITCH_TAB);
}
function closeTab(id) {
// If closing tab was active, switch tab or close window
if (id == activeTabId) {
if (tabs.size == 1) {
// Last tab is closing, shut window down
tabs.delete(id);
closeWindow();
return;
}
// Other tabs are open, switch to rightmost tab
var tabsEntries = Array.from(tabs.entries());
var lastEntry = tabsEntries.pop();
if (lastEntry[0] == id) {
lastEntry = tabsEntries.pop();
}
switchToTab(lastEntry[0], true);
}
// Remove tab element
var tabElement = document.getElementById(`tab-${id}`);
if (tabElement) {
tabElement.parentNode.removeChild(tabElement);
}
// Remove tab from map
tabs.delete(id);
var message = {
message: commands.MG_CLOSE_TAB,
args: {
tabId: id
}
};
window.chrome.webview.postMessage(message);
}
function updateFaviconURI(tabId, src) {
let tab = tabs.get(tabId);
if (tab.favicon != src) {
let img = new Image();
// Update favicon element on successful load
img.onload = () => {
console.log('Favicon loaded');
tab.favicon = src;
if (tabId == activeTabId) {
updatedFaviconURIHandler(tabId, tab);
}
};
if (src) {
// Try load from root on failed load
img.onerror = () => {
console.log('Cannot load favicon from link, trying with root');
updateFaviconURI(tabId, '');
};
} else {
// No link for favicon, try loading from root
try {
let tabURI = new URL(tab.uri);
src = `${tabURI.origin}/favicon.ico`;
} catch(e) {
console.log(`Could not parse tab ${tabId} URI`);
}
img.onerror = () => {
console.log('No favicon in site root. Using default favicon.');
tab.favicon = 'img/favicon.png';
updatedFaviconURIHandler(tabId, tab);
};
}
img.src = src;
}
}
function updatedFaviconURIHandler(tabId, tab) {
updateNavigationUI(commands.MG_UPDATE_FAVICON);
// Update favicon in history item
if (tab.historyItemId != INVALID_HISTORY_ID) {
updateHistoryItem(tab.historyItemId, historyItemFromTab(tabId));
}
}
function favoriteFromTab(tabId) {
if (!isValidTabId(tabId)) {
console.log('Invalid tab ID');
return;
}
let tab = tabs.get(tabId);
let favicon = tab.favicon == 'img/favicon.png' ? '../controls_ui/' + tab.favicon : tab.favicon;
return {
uri: tab.uri,
uriToShow: tab.uriToShow,
title: tab.title,
favicon: favicon
};
}
function historyItemFromTab(tabId) {
if (!isValidTabId(tabId)) {
console.log('Invalid tab ID');
return;
}
let tab = tabs.get(tabId);
let favicon = tab.favicon == 'img/favicon.png' ? '../controls_ui/' + tab.favicon : tab.favicon;
return {
uri: tab.uri,
title: tab.title,
favicon: favicon,
timestamp: new Date()
}
}