-
-
Add Shortcut
-
-
-
-
-
Toggle Anonymous Mode
@@ -576,6 +595,13 @@
+
+
History Page
+
+
+
+
+
Quick URL
@@ -587,6 +613,49 @@
+
+
Privacy
+
+
+
+
+
Anonymous Mode
+
Hide your real name and use a randomly generated one
+
+
+
+
+
+
Password Protected Shortcuts
+
Add password protection to specific shortcuts
+
+
+
+
+
Master Password
+
Set a master password for your protected shortcuts
+
+
+
+
+
+
+
+
Backgrounds
@@ -707,6 +776,56 @@
+
+
+
+
+
+
+
+
+
Confirm Action
+
Are you sure you want to proceed with this action?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Password Required
+
This shortcut is password protected. Please enter the password to continue.
+
+

+
+ Try
Secrecy for full browser password protection.
+ JSTAR Tab only protects shortcuts within this page.
+
+
+
+
+
+
Incorrect password. Please try again.
+
+
+
+
+
+
+
+
+
@@ -730,12 +849,6 @@
-
-
-
-
-
-
@@ -744,6 +857,15 @@
+
+
+
+
+
+
+
+
+
@@ -789,24 +911,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -876,10 +980,205 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/grid-layout.js b/js/grid-layout.js
index 44734ae..8d4b9d7 100644
--- a/js/grid-layout.js
+++ b/js/grid-layout.js
@@ -179,7 +179,11 @@ const GridLayout = {
const resetButton = document.getElementById('reset-layout');
if (resetButton) {
resetButton.addEventListener('click', () => {
- this.resetToDefaults();
+ shortcuts.showConfirmDialog(
+ 'Reset Layout',
+ 'Are you sure you want to reset the layout settings to default?',
+ () => this.resetToDefaults()
+ );
});
}
},
@@ -325,10 +329,12 @@ const GridLayout = {
};
document.addEventListener('DOMContentLoaded', () => {
+ console.log('DOM content loaded - initializing grid layout');
GridLayout.init();
});
window.addEventListener('load', () => {
+ console.log('Window fully loaded - ensuring grid layout is initialized');
setTimeout(() => {
if (!window.gridLayoutInitialized) {
GridLayout.init();
diff --git a/js/history.js b/js/history.js
new file mode 100644
index 0000000..7c22e18
--- /dev/null
+++ b/js/history.js
@@ -0,0 +1,904 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const historyContainer = document.querySelector('.history-container');
+ const searchInput = document.querySelector('.search-bar input');
+ const searchClear = document.querySelector('.search-clear');
+ const selectAllCheckbox = document.querySelector('#select-all');
+ const deleteSelectedBtn = document.querySelector('#delete-selected');
+ const clearAllBtn = document.querySelector('#clear-all');
+ const historyContent = document.querySelector('.history-content');
+ const historyItems = document.querySelector('.history-items');
+ const historyLoading = document.querySelector('.history-loading');
+ const historyEmpty = document.querySelector('.history-empty');
+ const loadMoreBtn = document.querySelector('.history-load-more button');
+ const confirmationDialog = document.querySelector('#confirmation-dialog');
+ const confirmTitle = document.querySelector('#confirmation-title');
+ const confirmMessage = document.querySelector('#confirmation-message');
+ const confirmYesBtn = document.querySelector('#confirm-yes');
+ const confirmNoBtn = document.querySelector('#confirm-no');
+ const searchResultsCount = document.querySelector('.search-results-count');
+
+ const browserInfo = detectBrowser();
+ console.log(`Detected browser: ${browserInfo.name} ${browserInfo.version}`);
+
+ const faviconCache = new Map();
+
+ let state = {
+ history: [],
+ filteredHistory: [],
+ searchTerm: '',
+ selectedItems: new Set(),
+ isLoading: true,
+ page: 1,
+ hasMore: true,
+ itemsPerPage: 100,
+ currentAction: null
+ };
+
+ initEvents();
+ loadHistory();
+
+ function initEvents() {
+ if (searchInput) searchInput.addEventListener('input', debounce(handleSearch, 300));
+ if (searchClear) {
+ searchClear.addEventListener('click', clearSearch);
+ hideElement(searchClear);
+ }
+
+ if (selectAllCheckbox) selectAllCheckbox.addEventListener('change', handleSelectAll);
+
+ if (deleteSelectedBtn) deleteSelectedBtn.addEventListener('click', () => {
+ if (state.selectedItems.size === 0) {
+ showNotification('No items selected', 'error');
+ return;
+ }
+ state.currentAction = 'delete-selected';
+ confirmTitle.textContent = 'Delete selected items?';
+ confirmMessage.textContent = 'This will remove the selected browsing history items.';
+ showConfirmationDialog();
+ });
+
+ if (clearAllBtn) clearAllBtn.addEventListener('click', () => {
+ if (state.filteredHistory.length === 0) {
+ showNotification('No history to clear', 'error');
+ return;
+ }
+ state.currentAction = 'clear-all';
+ confirmTitle.textContent = 'Clear all history?';
+ confirmMessage.textContent = 'This will remove all your browsing history.';
+ showConfirmationDialog();
+ });
+
+ if (loadMoreBtn) loadMoreBtn.addEventListener('click', loadMoreHistory);
+
+ if (confirmYesBtn) confirmYesBtn.addEventListener('click', handleConfirmedDelete);
+ if (confirmNoBtn) confirmNoBtn.addEventListener('click', hideConfirmationDialog);
+
+ const modalBackdrop = document.querySelector('.modal-backdrop');
+ if (modalBackdrop) {
+ modalBackdrop.addEventListener('click', hideConfirmationDialog);
+ }
+
+ const backButton = document.querySelector('.back-button');
+ if (backButton) {
+ backButton.addEventListener('click', () => {
+ window.history.back();
+ });
+ }
+ }
+
+ function detectBrowser() {
+ const userAgent = navigator.userAgent;
+ let browserName = "Unknown";
+ let version = "";
+
+ if (userAgent.indexOf("Firefox") > -1) {
+ browserName = "Firefox";
+ version = userAgent.match(/Firefox\/([0-9.]+)/)[1];
+ } else if (userAgent.indexOf("Edg") > -1) {
+ browserName = "Edge";
+ version = userAgent.match(/Edg\/([0-9.]+)/)[1];
+ } else if (userAgent.indexOf("Chrome") > -1) {
+ browserName = "Chrome";
+ version = userAgent.match(/Chrome\/([0-9.]+)/)[1];
+ } else if (userAgent.indexOf("Safari") > -1) {
+ browserName = "Safari";
+ version = userAgent.match(/Safari\/([0-9.]+)/)[1];
+ } else if (userAgent.indexOf("OPR") > -1 || userAgent.indexOf("Opera") > -1) {
+ browserName = "Opera";
+ version = userAgent.match(/(?:OPR|Opera)\/([0-9.]+)/)[1];
+ }
+
+ return { name: browserName, version: version };
+ }
+
+ function extractDomain(url) {
+ try {
+ const urlObj = new URL(url);
+ return urlObj.hostname;
+ } catch (e) {
+ return url;
+ }
+ }
+
+ async function loadHistory() {
+ showLoading();
+
+ try {
+ state.page = 1;
+ state.hasMore = true;
+
+ if (typeof chrome !== 'undefined' && chrome.history) {
+ console.log('Using Chrome history API');
+ const historyData = await fetchChromeHistory();
+ state.history = historyData;
+ state.filteredHistory = historyData;
+
+ if (historyData.length >= state.itemsPerPage) {
+ state.hasMore = true;
+ } else {
+ state.hasMore = false;
+ }
+ } else if (typeof browser !== 'undefined' && browser.history) {
+ console.log('Using Firefox history API');
+ const historyData = await fetchFirefoxHistory();
+ state.history = historyData;
+ state.filteredHistory = historyData;
+
+ if (historyData.length >= state.itemsPerPage) {
+ state.hasMore = true;
+ } else {
+ state.hasMore = false;
+ }
+ } else {
+ console.warn('No browser history API available');
+ state.history = [];
+ state.filteredHistory = [];
+ showNotification('Cannot access browser history. Make sure permissions are granted.', 'error');
+ }
+
+ state.isLoading = false;
+
+ updateUI();
+
+ if (state.filteredHistory.length === 0) {
+ if (state.searchTerm) {
+ showEmptyState('No history found', 'No results match your search. Try different keywords.');
+ } else {
+ showEmptyState('No history found', 'Your browsing history will appear here.');
+ }
+ }
+ } catch (error) {
+ console.error('Error loading history:', error);
+ hideLoading();
+ showEmptyState('Error loading history', 'There was a problem accessing your browsing history. Make sure the extension has the necessary permissions.');
+ }
+ }
+
+ async function fetchChromeHistory() {
+ return new Promise((resolve, reject) => {
+ const searchParams = getHistorySearchParams();
+
+ chrome.history.search(searchParams, (historyItems) => {
+ if (chrome.runtime.lastError) {
+ reject(chrome.runtime.lastError);
+ return;
+ }
+
+ const formattedItems = historyItems.map(item => ({
+ id: item.id,
+ url: item.url,
+ title: item.title || extractDomain(item.url),
+ lastVisitTime: item.lastVisitTime,
+ visitCount: item.visitCount,
+ domain: extractDomain(item.url)
+ }));
+
+ resolve(formattedItems);
+ });
+ });
+ }
+
+ async function fetchFirefoxHistory() {
+ return new Promise((resolve, reject) => {
+ const searchParams = getHistorySearchParams();
+
+ browser.history.search(searchParams).then(historyItems => {
+ const formattedItems = historyItems.map(item => ({
+ id: item.id,
+ url: item.url,
+ title: item.title || extractDomain(item.url),
+ lastVisitTime: item.lastVisitTime,
+ visitCount: item.visitCount,
+ domain: extractDomain(item.url)
+ }));
+
+ resolve(formattedItems);
+ }).catch(error => {
+ reject(error);
+ });
+ });
+ }
+
+ function getHistorySearchParams() {
+ return {
+ text: state.searchTerm,
+ maxResults: state.itemsPerPage * 3,
+ startTime: 0
+ };
+ }
+
+ async function loadMoreHistory() {
+ if (!state.hasMore) {
+ hideElement(document.querySelector('.history-load-more'));
+ return;
+ }
+
+ state.page++;
+
+ if (typeof chrome !== 'undefined' && chrome.history) {
+ try {
+ const lastItem = state.filteredHistory[state.filteredHistory.length - 1];
+ const startTime = lastItem ? lastItem.lastVisitTime - 1 : 0;
+
+ const searchParams = {
+ text: state.searchTerm,
+ maxResults: state.itemsPerPage * 2,
+ startTime: 0,
+ endTime: startTime
+ };
+
+ chrome.history.search(searchParams, (historyItems) => {
+ if (chrome.runtime.lastError) {
+ console.error('Error loading more history:', chrome.runtime.lastError);
+ return;
+ }
+
+ const existingIds = new Set(state.filteredHistory.map(item => item.id));
+ const newItems = historyItems
+ .filter(item => !existingIds.has(item.id))
+ .map(item => ({
+ id: item.id,
+ url: item.url,
+ title: item.title || extractDomain(item.url),
+ lastVisitTime: item.lastVisitTime,
+ visitCount: item.visitCount,
+ domain: extractDomain(item.url)
+ }));
+
+ if (newItems.length === 0) {
+ state.hasMore = false;
+ hideElement(document.querySelector('.history-load-more'));
+ return;
+ }
+
+ state.history = [...state.history, ...newItems];
+ state.filteredHistory = [...state.filteredHistory, ...newItems];
+
+ renderMoreHistoryItems(newItems);
+
+ if (newItems.length < state.itemsPerPage) {
+ state.hasMore = false;
+ hideElement(document.querySelector('.history-load-more'));
+ }
+ });
+ } catch (error) {
+ console.error('Error loading more history:', error);
+ state.hasMore = false;
+ hideElement(document.querySelector('.history-load-more'));
+ }
+ } else if (typeof browser !== 'undefined' && browser.history) {
+ try {
+ const lastItem = state.filteredHistory[state.filteredHistory.length - 1];
+ const startTime = lastItem ? lastItem.lastVisitTime - 1 : 0;
+
+ const searchParams = {
+ text: state.searchTerm,
+ maxResults: state.itemsPerPage * 2,
+ startTime: 0,
+ endTime: startTime
+ };
+
+ const historyItems = await browser.history.search(searchParams);
+
+ const existingIds = new Set(state.filteredHistory.map(item => item.id));
+ const newItems = historyItems
+ .filter(item => !existingIds.has(item.id))
+ .map(item => ({
+ id: item.id,
+ url: item.url,
+ title: item.title || extractDomain(item.url),
+ lastVisitTime: item.lastVisitTime,
+ visitCount: item.visitCount,
+ domain: extractDomain(item.url)
+ }));
+
+ if (newItems.length === 0) {
+ state.hasMore = false;
+ hideElement(document.querySelector('.history-load-more'));
+ return;
+ }
+
+ state.history = [...state.history, ...newItems];
+ state.filteredHistory = [...state.filteredHistory, ...newItems];
+
+ renderMoreHistoryItems(newItems);
+
+ if (newItems.length < state.itemsPerPage) {
+ state.hasMore = false;
+ hideElement(document.querySelector('.history-load-more'));
+ }
+ } catch (error) {
+ console.error('Error loading more history:', error);
+ state.hasMore = false;
+ hideElement(document.querySelector('.history-load-more'));
+ }
+ }
+ }
+
+ function renderMoreHistoryItems(items) {
+ if (items.length === 0) return;
+
+ const groupedItems = groupByDate(items);
+ const historyItemsContainer = document.querySelector('.history-items');
+
+ for (const [date, dateItems] of Object.entries(groupedItems)) {
+ let dateGroup = document.querySelector(`.history-date-group[data-date="${date}"]`);
+
+ if (!dateGroup) {
+ dateGroup = document.createElement('div');
+ dateGroup.className = 'history-date-group';
+ dateGroup.setAttribute('data-date', date);
+
+ const dateHeader = document.createElement('div');
+ dateHeader.className = 'history-date-header';
+ dateHeader.textContent = formatDateHeading(date);
+
+ dateGroup.appendChild(dateHeader);
+ historyItemsContainer.appendChild(dateGroup);
+ }
+
+ for (const item of dateItems) {
+ const itemElement = createHistoryItemElement(item);
+ dateGroup.appendChild(itemElement);
+ }
+ }
+ }
+
+ function handleSearch() {
+ const searchTerm = searchInput.value.trim().toLowerCase();
+ state.searchTerm = searchTerm;
+
+ if (searchTerm.length > 0) {
+ showElement(searchClear);
+ } else {
+ hideElement(searchClear);
+ }
+
+ state.page = 1;
+ state.hasMore = true;
+
+ loadHistory();
+ }
+
+ function clearSearch() {
+ if (searchInput) {
+ searchInput.value = '';
+ state.searchTerm = '';
+ hideElement(searchClear);
+
+ state.page = 1;
+ state.hasMore = true;
+
+ loadHistory();
+ }
+ }
+
+ function applySearch() {
+ if (state.searchTerm.length > 0) {
+ const filtered = state.history.filter(item => {
+ const title = (item.title || '').toLowerCase();
+ const url = (item.url || '').toLowerCase();
+ const domain = (item.domain || '').toLowerCase();
+
+ return title.includes(state.searchTerm) ||
+ url.includes(state.searchTerm) ||
+ domain.includes(state.searchTerm);
+ });
+
+ state.filteredHistory = filtered;
+ } else {
+ state.filteredHistory = state.history;
+ }
+
+ updateUI();
+ }
+
+ function updateUI() {
+ hideLoading();
+
+ if (state.filteredHistory.length === 0) {
+ showEmptyState('No history found', 'No results match your search. Try different keywords.');
+ } else {
+ hideEmptyState();
+ }
+
+ renderHistoryItems(state.filteredHistory);
+
+ updateActionButtonsState();
+
+ if (state.hasMore) {
+ showElement(document.querySelector('.history-load-more'));
+ } else {
+ hideElement(document.querySelector('.history-load-more'));
+ }
+
+ if (searchResultsCount) {
+ if (state.searchTerm) {
+ searchResultsCount.textContent = `${state.filteredHistory.length} results found for "${state.searchTerm}"`;
+ showElement(searchResultsCount);
+ } else {
+ hideElement(searchResultsCount);
+ }
+ }
+ }
+
+ function renderHistoryItems(items) {
+ if (!historyItems) return;
+
+ historyItems.innerHTML = '';
+
+ if (items.length === 0) {
+ showEmptyState();
+ return;
+ }
+
+ const groupedItems = groupByDate(items);
+
+ for (const [date, dateItems] of Object.entries(groupedItems)) {
+ const dateGroup = document.createElement('div');
+ dateGroup.className = 'history-date-group';
+ dateGroup.setAttribute('data-date', date);
+
+ const dateHeader = document.createElement('div');
+ dateHeader.className = 'history-date-header';
+ dateHeader.textContent = formatDateHeading(date);
+
+ dateGroup.appendChild(dateHeader);
+
+ for (const item of dateItems) {
+ const itemElement = createHistoryItemElement(item);
+ dateGroup.appendChild(itemElement);
+ }
+
+ historyItems.appendChild(dateGroup);
+ }
+ }
+
+ function createHistoryItemElement(item) {
+ const historyItem = document.createElement('div');
+ historyItem.className = 'history-item';
+ historyItem.setAttribute('data-id', item.id);
+
+ const checkbox = document.createElement('div');
+ checkbox.className = 'history-item-checkbox';
+ const checkboxInput = document.createElement('input');
+ checkboxInput.type = 'checkbox';
+ checkboxInput.checked = state.selectedItems.has(item.id);
+ checkboxInput.addEventListener('change', (e) => {
+ handleItemSelection(item.id, e.target.checked);
+ });
+ checkbox.appendChild(checkboxInput);
+
+ const favicon = document.createElement('div');
+ favicon.className = 'history-item-favicon';
+
+ let faviconUrl = faviconCache.get(item.domain);
+ if (!faviconUrl) {
+ faviconUrl = `https://www.google.com/s2/favicons?domain=${item.domain}&sz=32`;
+ faviconCache.set(item.domain, faviconUrl);
+ }
+
+ const faviconImg = document.createElement('img');
+ faviconImg.src = faviconUrl;
+ faviconImg.alt = '';
+ faviconImg.onerror = () => {
+ favicon.innerHTML = `
+
+ `;
+ };
+
+ favicon.appendChild(faviconImg);
+
+ const content = document.createElement('div');
+ content.className = 'history-item-content';
+
+ const title = document.createElement('div');
+ title.className = 'history-item-title';
+ title.textContent = item.title || extractDomain(item.url);
+
+ const url = document.createElement('div');
+ url.className = 'history-item-url';
+ url.textContent = item.url;
+
+ content.appendChild(title);
+ content.appendChild(url);
+
+ const time = document.createElement('div');
+ time.className = 'history-item-time';
+ time.textContent = formatTime(new Date(item.lastVisitTime));
+
+ const actions = document.createElement('div');
+ actions.className = 'history-item-actions';
+
+ const openAction = document.createElement('button');
+ openAction.className = 'history-item-action';
+ openAction.title = 'Open in new tab';
+ openAction.innerHTML = '
';
+ openAction.addEventListener('click', (e) => {
+ e.stopPropagation();
+ openLink(item.url);
+ });
+
+ const deleteAction = document.createElement('button');
+ deleteAction.className = 'history-item-action delete';
+ deleteAction.title = 'Delete';
+ deleteAction.innerHTML = '
';
+ deleteAction.addEventListener('click', (e) => {
+ e.stopPropagation();
+ deleteHistoryItem(item.id);
+ });
+
+ actions.appendChild(openAction);
+ actions.appendChild(deleteAction);
+
+ content.addEventListener('click', () => {
+ const isSelected = state.selectedItems.has(item.id);
+ checkboxInput.checked = !isSelected;
+ handleItemSelection(item.id, !isSelected);
+ });
+
+ historyItem.appendChild(checkbox);
+ historyItem.appendChild(favicon);
+ historyItem.appendChild(content);
+ historyItem.appendChild(time);
+ historyItem.appendChild(actions);
+
+ return historyItem;
+ }
+
+ function handleSelectAll(e) {
+ const isChecked = e.target.checked;
+
+ if (isChecked) {
+ state.filteredHistory.forEach(item => {
+ state.selectedItems.add(item.id);
+ });
+ } else {
+ state.selectedItems.clear();
+ }
+
+ const checkboxes = document.querySelectorAll('.history-item-checkbox input');
+ checkboxes.forEach(checkbox => {
+ const itemId = checkbox.closest('.history-item').getAttribute('data-id');
+ checkbox.checked = state.selectedItems.has(itemId);
+ });
+
+ updateActionButtonsState();
+ }
+
+ function handleItemSelection(id, isSelected) {
+ if (isSelected) {
+ state.selectedItems.add(id);
+ } else {
+ state.selectedItems.delete(id);
+ }
+
+ updateActionButtonsState();
+ }
+
+ function updateActionButtonsState() {
+ if (deleteSelectedBtn) {
+ deleteSelectedBtn.disabled = state.selectedItems.size === 0;
+ }
+
+ if (clearAllBtn) {
+ clearAllBtn.disabled = state.filteredHistory.length === 0;
+ }
+
+ const selectAllCheckbox = document.querySelector('#select-all');
+ if (selectAllCheckbox) {
+ if (state.filteredHistory.length === 0) {
+ selectAllCheckbox.checked = false;
+ selectAllCheckbox.disabled = true;
+ } else {
+ selectAllCheckbox.disabled = false;
+ selectAllCheckbox.checked = state.filteredHistory.length > 0 &&
+ state.selectedItems.size === state.filteredHistory.length;
+ }
+ }
+ }
+
+ function deleteHistoryItem(id) {
+ state.currentAction = 'delete-item';
+ state.currentItemId = id;
+ confirmTitle.textContent = 'Delete item?';
+ confirmMessage.textContent = 'This will remove this item from your browsing history.';
+ showConfirmationDialog();
+ }
+
+ function removeHistoryItemFromState(id) {
+ state.selectedItems.delete(id);
+
+ state.history = state.history.filter(item => item.id !== id);
+ state.filteredHistory = state.filteredHistory.filter(item => item.id !== id);
+
+ const itemElement = document.querySelector(`.history-item[data-id="${id}"]`);
+ if (itemElement) {
+ const dateGroup = itemElement.closest('.history-date-group');
+ itemElement.remove();
+
+ if (dateGroup && dateGroup.querySelectorAll('.history-item').length === 0) {
+ dateGroup.remove();
+ }
+ }
+
+ updateActionButtonsState();
+
+ if (state.filteredHistory.length === 0) {
+ if (state.searchTerm) {
+ showEmptyState('No history found', 'No results match your search. Try different keywords.');
+ } else {
+ showEmptyState('No history found', 'Your browsing history will appear here.');
+ }
+ }
+ }
+
+ function handleConfirmedDelete() {
+ hideConfirmationDialog();
+
+ if (state.currentAction === 'delete-selected') {
+ deleteSelectedItems();
+ } else if (state.currentAction === 'clear-all') {
+ clearAllHistory();
+ } else if (state.currentAction === 'delete-item') {
+ const id = state.currentItemId;
+ if (typeof chrome !== 'undefined' && chrome.history) {
+ chrome.history.deleteUrl({ url: state.history.find(item => item.id === id).url }, () => {
+ removeHistoryItemFromState(id);
+ showNotification('Item deleted', 'success');
+ });
+ } else if (typeof browser !== 'undefined' && browser.history) {
+ browser.history.deleteUrl({ url: state.history.find(item => item.id === id).url }).then(() => {
+ removeHistoryItemFromState(id);
+ showNotification('Item deleted', 'success');
+ });
+ } else {
+ removeHistoryItemFromState(id);
+ showNotification('Item deleted', 'success');
+ }
+ }
+
+ state.currentAction = null;
+ state.currentItemId = null;
+ }
+
+ function deleteSelectedItems() {
+ const selectedIds = Array.from(state.selectedItems);
+
+ if (selectedIds.length === 0) {
+ showNotification('No items selected', 'error');
+ return;
+ }
+
+ const selectedUrls = state.history
+ .filter(item => state.selectedItems.has(item.id))
+ .map(item => item.url);
+
+ if (typeof chrome !== 'undefined' && chrome.history) {
+ let deletedCount = 0;
+
+ selectedUrls.forEach(url => {
+ chrome.history.deleteUrl({ url }, () => {
+ deletedCount++;
+ if (deletedCount === selectedUrls.length) {
+ selectedIds.forEach(id => removeHistoryItemFromState(id));
+ state.selectedItems.clear();
+ updateActionButtonsState();
+
+ showNotification(`${selectedUrls.length} items deleted`, 'success');
+ }
+ });
+ });
+ } else if (typeof browser !== 'undefined' && browser.history) {
+ Promise.all(selectedUrls.map(url => browser.history.deleteUrl({ url })))
+ .then(() => {
+ selectedIds.forEach(id => removeHistoryItemFromState(id));
+ state.selectedItems.clear();
+ updateActionButtonsState();
+
+ showNotification(`${selectedUrls.length} items deleted`, 'success');
+ })
+ .catch(error => {
+ console.error('Error deleting history items:', error);
+ showNotification('Error deleting items', 'error');
+ });
+ } else {
+ selectedIds.forEach(id => removeHistoryItemFromState(id));
+ state.selectedItems.clear();
+ updateActionButtonsState();
+
+ showNotification(`${selectedUrls.length} items deleted`, 'success');
+ }
+ }
+
+ function clearAllHistory() {
+ if (state.filteredHistory.length === 0) {
+ showNotification('No history to clear', 'error');
+ return;
+ }
+
+ if (typeof chrome !== 'undefined' && chrome.history) {
+ chrome.history.deleteAll(() => {
+ state.history = [];
+ state.filteredHistory = [];
+ state.selectedItems.clear();
+
+ historyItems.innerHTML = '';
+ updateActionButtonsState();
+ showEmptyState('History cleared', 'Your browsing history has been cleared.');
+
+ showNotification('History cleared', 'success');
+ });
+ } else if (typeof browser !== 'undefined' && browser.history) {
+ browser.history.deleteAll().then(() => {
+ state.history = [];
+ state.filteredHistory = [];
+ state.selectedItems.clear();
+
+ historyItems.innerHTML = '';
+ updateActionButtonsState();
+ showEmptyState('History cleared', 'Your browsing history has been cleared.');
+
+ showNotification('History cleared', 'success');
+ });
+ } else {
+ state.history = [];
+ state.filteredHistory = [];
+ state.selectedItems.clear();
+
+ historyItems.innerHTML = '';
+ updateActionButtonsState();
+ showEmptyState('History cleared', 'Your browsing history has been cleared.');
+
+ showNotification('History cleared', 'success');
+ }
+ }
+
+ function showLoading() {
+ if (historyLoading) {
+ showElement(historyLoading);
+ }
+
+ hideEmptyState();
+ if (historyItems) {
+ historyItems.innerHTML = '';
+ }
+ }
+
+ function hideLoading() {
+ if (historyLoading) {
+ hideElement(historyLoading);
+ }
+ }
+
+ function showEmptyState(title = 'No history found', message = 'There are no items in your browsing history that match your search.') {
+ if (!historyEmpty) return;
+
+ const titleElement = historyEmpty.querySelector('h2');
+ const messageElement = historyEmpty.querySelector('p');
+
+ if (titleElement) titleElement.textContent = title;
+ if (messageElement) messageElement.textContent = message;
+
+ showElement(historyEmpty);
+ }
+
+ function hideEmptyState() {
+ if (historyEmpty) {
+ hideElement(historyEmpty);
+ }
+ }
+
+ function showElement(element) {
+ if (element) element.classList.remove('hidden');
+ }
+
+ function hideElement(element) {
+ if (element) element.classList.add('hidden');
+ }
+
+ function showConfirmationDialog() {
+ if (confirmationDialog) {
+ showElement(confirmationDialog);
+ }
+ }
+
+ function hideConfirmationDialog() {
+ if (confirmationDialog) {
+ hideElement(confirmationDialog);
+ }
+ }
+
+ function showNotification(message, type = 'success') {
+ if (window.notifications && typeof window.notifications.show === 'function') {
+ window.notifications.show(message, type);
+ } else {
+ console.log(`Notification: ${message} (${type})`);
+ }
+ }
+
+ function openLink(url) {
+ window.open(url, '_blank');
+ }
+
+ function formatDateHeading(dateStr) {
+ const date = new Date(dateStr);
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ if (date.toDateString() === today.toDateString()) {
+ return 'Today';
+ } else if (date.toDateString() === yesterday.toDateString()) {
+ return 'Yesterday';
+ } else {
+ return date.toLocaleDateString(undefined, {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ }
+ }
+
+ function formatTime(date) {
+ return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
+ }
+
+ function groupByDate(items) {
+ const grouped = {};
+
+ items.forEach(item => {
+ const date = new Date(item.lastVisitTime);
+ const dateKey = date.toDateString();
+
+ if (!grouped[dateKey]) {
+ grouped[dateKey] = [];
+ }
+
+ grouped[dateKey].push(item);
+ });
+
+ for (const date in grouped) {
+ grouped[date].sort((a, b) => b.lastVisitTime - a.lastVisitTime);
+ }
+
+ return grouped;
+ }
+
+ function debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ }
+});
\ No newline at end of file
diff --git a/js/keybinds.js b/js/keybinds.js
index 26e6a8f..e33d82e 100644
--- a/js/keybinds.js
+++ b/js/keybinds.js
@@ -11,13 +11,20 @@ const keybinds = {
init() {
this.bindings = Storage.get('keybinds') || {};
+ this.setupDefaultKeybinds();
+
const urlInput = document.getElementById('keybind-url');
const urlComboInput = document.getElementById('keybind-url-combo');
+ const historyComboInput = document.getElementById('keybind-history-combo');
if (this.bindings.url) {
urlInput.value = this.bindings.url.url || '';
urlComboInput.value = this.bindings.url.keys || '';
}
+
+ if (this.bindings.history && historyComboInput) {
+ historyComboInput.value = this.bindings.history.keys || '';
+ }
let lastSavedUrl = urlInput.value;
@@ -184,7 +191,7 @@ const keybinds = {
});
document.addEventListener('keydown', (e) => {
- if (e.target.tagName === 'INPUT') return;
+ if (e.target.tagName === 'INPUT' || !Storage.get('onboardingComplete')) return;
const keys = [];
if (e.altKey) keys.push('Alt');
@@ -201,10 +208,44 @@ const keybinds = {
});
});
},
+
+ setupDefaultKeybinds() {
+ const defaultBindings = {
+ settings: { keys: 'Shift+S' },
+ anonymous: { keys: 'Shift+X' },
+ theme: { keys: 'Shift+T' },
+ history: { keys: 'Shift+H' },
+ url: { keys: 'Shift+Q', url: '' }
+ };
+
+ Object.entries(defaultBindings).forEach(([action, binding]) => {
+ if (!this.bindings[action]) {
+ this.bindings[action] = binding;
+ }
+ });
+
+ Storage.set('keybinds', this.bindings);
+ },
executeAction(action, binding) {
+ if (!Storage.get('onboardingComplete')) return;
+
const settingsPage = document.getElementById('settings-page');
- if (settingsPage.classList.contains('active')) {
+ const passwordDialog = document.getElementById('password-dialog');
+
+ if (passwordDialog && !passwordDialog.classList.contains('hidden')) {
+ const cancelBtn = document.getElementById('cancel-password');
+ if (cancelBtn) {
+ cancelBtn.click();
+ } else {
+ passwordDialog.classList.remove('active');
+ setTimeout(() => {
+ passwordDialog.classList.add('hidden');
+ }, 300);
+ }
+ }
+
+ if (action === 'settings' && settingsPage.classList.contains('active')) {
settingsPage.classList.remove('active');
setTimeout(() => {
settingsPage.classList.add('hidden');
@@ -216,50 +257,55 @@ const keybinds = {
switch (action) {
case 'settings':
if (settingsPage.classList.contains('hidden')) {
- notifications.show('Opening settings.', 'info');
settings.updateSettingsUI();
settingsPage.classList.remove('hidden');
setTimeout(() => {
settingsPage.classList.add('active');
}, 10);
} else {
- notifications.show('Settings closed.', 'info');
settings.updateSettingsUI();
}
break;
case 'add-shortcut':
const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
- notifications.show('Maximum shortcuts limit reached!', 'error');
return;
}
const shortcutModal = document.getElementById('add-shortcut-modal');
if (shortcutModal === activeModal) {
- notifications.show('Add shortcut menu closed.', 'info');
+ closeModal(shortcutModal);
} else {
- notifications.show('Opening add shortcut menu.', 'info');
openModal(shortcutModal);
}
break;
case 'anonymous':
- settings.toggleAnonymousMode();
+ const isAnonymous = Storage.get('anonymousMode') || false;
+ Storage.set('anonymousMode', !isAnonymous);
+
+ if (!isAnonymous) {
+ const randomName = anonymousNames.generate();
+ Storage.set('anonymousName', randomName);
+ } else {
+ Storage.remove('anonymousName');
+ }
+
+ shortcuts.render();
+ updateGreeting();
break;
case 'theme':
settings.toggleTheme();
break;
+ case 'history':
+ window.location.href = 'history.html';
+ break;
case 'url':
if (binding.url) {
const url = binding.url;
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ?
url : `https://${url}`;
-
- notifications.show(`Redirecting to ${url}...`, 'info');
- setTimeout(() => {
- window.location.href = fullUrl;
- }, 1000);
- } else {
- notifications.show('No URL set for this keybind.', 'error');
+
+ window.location.href = fullUrl;
}
break;
}
diff --git a/js/onboarding.js b/js/onboarding.js
index 8854d8d..955da6b 100644
--- a/js/onboarding.js
+++ b/js/onboarding.js
@@ -293,6 +293,16 @@ const onboarding = {
Storage.set(key, value);
});
+ if (!Storage.get('keybinds')) {
+ Storage.set('keybinds', {
+ settings: { keys: 'Shift+S' },
+ anonymous: { keys: 'Shift+X' },
+ theme: { keys: 'Shift+T' },
+ history: { keys: 'Shift+H' },
+ url: { keys: 'Shift+Q', url: '' }
+ });
+ }
+
Storage.set('onboardingComplete', true);
setTimeout(() => {
diff --git a/js/settings.js b/js/settings.js
index 30f2498..2858ff8 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -4,6 +4,7 @@ const defaultSettings = {
anonymousMode: false,
searchEngine: 'Google',
updateAlerts: true,
+ motionPreference: 'default',
show_greeting: true,
show_search: true,
show_shortcuts: true,
@@ -43,6 +44,13 @@ const defaultSettings = {
'--toggle-bg': '#333333',
'--toggle-bg-active': 'var(--text)',
'--toggle-knob': 'var(--background)'
+ },
+ keybinds: {
+ settings: { keys: 'Shift+S' },
+ anonymous: { keys: 'Shift+X' },
+ theme: { keys: 'Shift+T' },
+ history: { keys: 'Shift+H' },
+ url: { keys: 'Shift+Q', url: '' }
}
};
@@ -81,20 +89,93 @@ document.addEventListener('DOMContentLoaded', () => {
updateAlerts !== false;
});
+const updateIconStyle = (style) => {
+ const icons = document.querySelectorAll('svg use');
+ icons.forEach(icon => {
+ const iconId = icon.getAttribute('href').substring(1);
+ let newIconId;
+
+ if (style === 'solid') {
+ if (iconId.endsWith('-solid')) {
+ newIconId = iconId;
+ } else {
+ newIconId = `${iconId}-solid`;
+ }
+ } else {
+ newIconId = iconId.endsWith('-solid') ? iconId.slice(0, -6) : iconId;
+ }
+
+ icon.setAttribute('href', `#${newIconId}`);
+ });
+};
+
+const savedIconStyle = Storage.get('iconStyle') || 'linear';
+document.getElementById('icon-style-select').value = savedIconStyle;
+updateIconStyle(savedIconStyle);
+
+document.getElementById('icon-style-select').addEventListener('change', (event) => {
+ const selectedStyle = event.target.value;
+ Storage.set('iconStyle', selectedStyle);
+ updateIconStyle(selectedStyle);
+});
+
+const savedMotionPreference = Storage.get('motionPreference') || 'default';
+document.getElementById('motion-preference-select').value = savedMotionPreference;
+updateMotionPreference(savedMotionPreference);
+
+document.getElementById('motion-preference-select').addEventListener('change', (event) => {
+ const selectedPreference = event.target.value;
+ Storage.set('motionPreference', selectedPreference);
+ updateMotionPreference(selectedPreference);
+ notifications.show(`Motion preference updated to ${selectedPreference}!`, 'success');
+});
+
+function updateMotionPreference(preference) {
+ document.body.classList.remove('subtle-motion', 'reduced-motion', 'minimal-motion', 'no-motion');
+
+ switch(preference) {
+ case 'subtle':
+ document.body.classList.add('subtle-motion');
+ break;
+ case 'reduced':
+ document.body.classList.add('reduced-motion');
+ break;
+ case 'minimal':
+ document.body.classList.add('minimal-motion');
+ break;
+ case 'disabled':
+ document.body.classList.add('no-motion');
+ break;
+ }
+}
+
const settings = {
GREETING_MAX_LENGTH: 60,
toggleTheme: () => {
const currentTheme = document.body.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+
+ const motionPreference = Storage.get('motionPreference') || 'default';
+ if (motionPreference !== 'default') {
+ document.body.classList.add('instant-theme-change');
+ }
+
document.body.setAttribute('data-theme', newTheme);
Storage.set('theme', newTheme);
+ const iconStyle = Storage.get('iconStyle') || 'linear';
const themeIcon = document.querySelector('#toggle-theme svg use');
- const lightModeIcon = newTheme === 'dark' ? 'dark-mode' : 'light-mode';
- const darkModeIcon = newTheme === 'dark' ? 'light-mode' : 'dark-mode';
+ const lightModeIcon = iconStyle === 'solid' ? 'icon-light-mode-solid' : 'icon-light-mode';
+ const darkModeIcon = iconStyle === 'solid' ? 'icon-dark-mode-solid' : 'icon-dark-mode';
- themeIcon.setAttribute('href', `#icon-${newTheme === 'dark' ? darkModeIcon : lightModeIcon}`);
+ themeIcon.setAttribute('href', `#${newTheme === 'dark' ? lightModeIcon : darkModeIcon}`);
+
+ if (motionPreference !== 'default') {
+ setTimeout(() => {
+ document.body.classList.remove('instant-theme-change');
+ }, 50);
+ }
},
@@ -304,9 +385,29 @@ const settings = {
fontSizeSlider.addEventListener('input', (e) => updateFontSize(e.target.value));
fontSizeNumber.addEventListener('change', (e) => updateFontSize(e.target.value));
- resetFontSize.addEventListener('click', settings.resetTypography);
- resetLightColors.addEventListener('click', () => settings.resetColors('light'));
- resetDarkColors.addEventListener('click', () => settings.resetColors('dark'));
+ resetFontSize.addEventListener('click', () => {
+ shortcuts.showConfirmDialog(
+ 'Reset Typography',
+ 'Are you sure you want to reset font settings to default?',
+ settings.resetTypography
+ );
+ });
+
+ resetLightColors.addEventListener('click', () => {
+ shortcuts.showConfirmDialog(
+ 'Reset Light Colors',
+ 'Are you sure you want to reset light mode colors to default?',
+ () => settings.resetColors('light')
+ );
+ });
+
+ resetDarkColors.addEventListener('click', () => {
+ shortcuts.showConfirmDialog(
+ 'Reset Dark Colors',
+ 'Are you sure you want to reset dark mode colors to default?',
+ () => settings.resetColors('dark')
+ );
+ });
settings.initColorSettings();
@@ -316,9 +417,10 @@ const settings = {
const savedTheme = Storage.get('theme') || 'light';
document.body.setAttribute('data-theme', savedTheme);
+ const iconStyle = Storage.get('iconStyle') || 'linear';
const themeIcon = document.querySelector('#toggle-theme svg use');
- const lightModeIcon = 'light-mode';
- const darkModeIcon = 'dark-mode';
+ const lightModeIcon = iconStyle === 'solid' ? 'light-mode-solid' : 'light-mode';
+ const darkModeIcon = iconStyle === 'solid' ? 'dark-mode-solid' : 'dark-mode';
themeIcon.setAttribute('href', `#icon-${savedTheme === 'dark' ? darkModeIcon : lightModeIcon}`);
@@ -369,11 +471,13 @@ const settings = {
const userName = Storage.get('userName') || '';
const isAnonymous = Storage.get('anonymousMode') || false;
const currentEngine = Storage.get('searchEngine') || 'google';
+ const masterPassword = Storage.get('masterPassword') || '';
document.getElementById('settings-name').value = userName;
document.getElementById('toggle-anonymous').checked = isAnonymous;
document.getElementById('search-engine-select').value = currentEngine;
document.getElementById('toggle-update-alerts').checked = updateAlerts;
+ document.getElementById('master-password').value = masterPassword;
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
const isVisible = Storage.get(`show_${element}`);
@@ -392,6 +496,7 @@ const settings = {
document.getElementById('font-family-select').value = fontFamily;
document.getElementById('font-size-slider').value = fontSize;
document.getElementById('font-size-number').value = fontSize;
+ document.getElementById('motion-preference-select').value = Storage.get('motionPreference') || 'default';
},
formatGreeting: (format) => {
@@ -450,6 +555,11 @@ const settings = {
exportData: () => {
try {
+ let masterPassword = Storage.get('masterPassword') || '';
+ if (masterPassword) {
+ masterPassword = btoa(masterPassword);
+ }
+
const data = {
settings: {
theme: Storage.get('theme') || defaultSettings.theme,
@@ -459,6 +569,11 @@ const settings = {
searchEngine: Storage.get('searchEngine') || 'Google',
customGreeting: Storage.get('customGreeting') || '',
updateAlerts: Storage.get('updateAlerts') !== false,
+ iconStyle: Storage.get('iconStyle') || 'linear',
+ motionPreference: Storage.get('motionPreference') || 'default',
+
+ passwordProtectionEnabled: Storage.get('passwordProtectionEnabled') || false,
+ masterPassword: masterPassword,
fontFamily: Storage.get('fontFamily') || defaultSettings.fontFamily,
fontSize: Storage.get('fontSize') || defaultSettings.fontSize,
@@ -649,6 +764,14 @@ const settings = {
}
});
+ if (data.settings.passwordProtectionEnabled) {
+ setTimeout(() => {
+ if (typeof shortcuts.createShortcutProtectionManager === 'function') {
+ shortcuts.createShortcutProtectionManager();
+ }
+ }, 100);
+ }
+
Storage.set('shortcuts', data.shortcuts);
const validatedKeybinds = {};
@@ -678,6 +801,8 @@ const settings = {
settings.initColorSettings();
}
+ updateIconStyle(data.settings.iconStyle || 'linear');
+
if (data.settings.fontFamily) {
document.documentElement.style.setProperty('--font-family', data.settings.fontFamily);
}
@@ -701,6 +826,18 @@ const settings = {
Storage.remove('customBackground');
}
+ let masterPassword = data.settings.masterPassword || '';
+ if (masterPassword) {
+ try {
+ masterPassword = atob(masterPassword);
+ Storage.set('masterPassword', masterPassword);
+ settings.updateSettingsUI();
+ } catch (e) {
+ console.error('Error decoding master password:', e);
+ notifications.show('Failed to decode master password!', 'error');
+ }
+ }
+
notifications.show('Data imported successfully!', 'success');
} catch (error) {
notifications.show('Failed to import data: Invalid file format!', 'error');
@@ -709,50 +846,27 @@ const settings = {
},
resetData: () => {
- Object.entries(defaultSettings).forEach(([key, value]) => {
- Storage.set(key, value);
- });
-
- Storage.remove('shortcuts');
- Storage.remove('keybinds');
- Storage.remove('anonymousName');
- Storage.remove('customGreeting');
-
- if (typeof GridLayout !== 'undefined') {
- if (GridLayout.reset) {
- GridLayout.reset();
- } else if (GridLayout.defaults) {
- Storage.set('gridLayout', GridLayout.defaults);
- if (GridLayout.init) {
+ shortcuts.showConfirmDialog(
+ 'Reset All Data',
+ 'Are you sure you want to reset all data? This action cannot be undone.',
+ () => {
+ try {
+ Storage.remove('customGreeting');
+ Storage.clear();
+
+ Storage.set('keybinds', defaultSettings.keybinds);
+
+ closeModal(document.getElementById('settings-modal'));
+ notifications.show('All data has been reset!', 'success');
setTimeout(() => {
- GridLayout.init();
- }, 100);
+ window.location.reload();
+ }, 1000);
+ } catch (error) {
+ notifications.show('Failed to reset data!', 'error');
+ console.error('Reset error:', error);
}
- } else {
- Storage.remove('gridLayout');
}
- } else {
- Storage.remove('gridLayout');
- }
-
- if (typeof GridLayout === 'undefined' || !GridLayout.reset) {
- const defaultVisibility = {
- showGreeting: true,
- showSearch: true,
- showShortcuts: true,
- showAddButton: true,
- showGrid: true
- };
- Storage.set('visibility', defaultVisibility);
- }
-
- settings.updateSettingsUI();
- settings.updateVisibility();
- settings.updateTypography();
- shortcuts.render();
- document.body.setAttribute('data-theme', 'light');
-
- notifications.show('All data has been reset!', 'success');
+ );
},
updateTypography: () => {
@@ -911,22 +1025,27 @@ settings.initDataManagement = () => {
});
resetBtn.addEventListener('click', () => {
- const confirmReset = confirm('Are you sure you want to reset all data? This action cannot be undone.');
-
- if (confirmReset) {
- try {
- Storage.remove('customGreeting');
- Storage.clear();
- closeModal(document.getElementById('settings-modal'));
- notifications.show('All data has been reset!', 'success');
- setTimeout(() => {
- window.location.reload();
- }, 1000);
- } catch (error) {
- notifications.show('Failed to reset data!', 'error');
- console.error('Reset error:', error);
+ shortcuts.showConfirmDialog(
+ 'Reset All Data',
+ 'Are you sure you want to reset all data? This action cannot be undone.',
+ () => {
+ try {
+ Storage.remove('customGreeting');
+ Storage.clear();
+
+ Storage.set('keybinds', defaultSettings.keybinds);
+
+ closeModal(document.getElementById('settings-modal'));
+ notifications.show('All data has been reset!', 'success');
+ setTimeout(() => {
+ window.location.reload();
+ }, 1000);
+ } catch (error) {
+ notifications.show('Failed to reset data!', 'error');
+ console.error('Reset error:', error);
+ }
}
- }
+ );
});
};
@@ -942,6 +1061,8 @@ function initCustomSelects() {
if (nativeSelect.id === 'search-engine-select') {
nativeSelect.value = Storage.get('searchEngine') || 'google';
+ } else if (nativeSelect.id === 'icon-style-select') {
+ nativeSelect.value = Storage.get('iconStyle') || 'linear';
} else if (nativeSelect.id === 'font-family-select') {
nativeSelect.value = Storage.get('fontFamily') || 'Inter';
selectedDiv.style.fontFamily = nativeSelect.value;
diff --git a/js/shortcuts.js b/js/shortcuts.js
index 995540e..e9d4031 100644
--- a/js/shortcuts.js
+++ b/js/shortcuts.js
@@ -14,7 +14,7 @@ const shortcuts = {
}
},
- add: (url, name) => {
+ add: (url, name, isPasswordProtected = false) => {
const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
notifications.show('Maximum shortcuts limit reached!', 'error');
@@ -27,7 +27,11 @@ const shortcuts = {
return;
}
- currentShortcuts.push({ url: formattedUrl, name });
+ currentShortcuts.push({
+ url: formattedUrl,
+ name,
+ isPasswordProtected: isPasswordProtected || false
+ });
Storage.set('shortcuts', currentShortcuts);
shortcuts.render();
CacheUpdater.update();
@@ -42,9 +46,131 @@ const shortcuts = {
CacheUpdater.update();
},
- edit: (index, newUrl, newName) => {
+ showConfirmDialog: (title, message, onConfirm) => {
+ const dialog = document.getElementById('confirmation-dialog');
+ const titleEl = document.getElementById('confirmation-title');
+ const messageEl = document.getElementById('confirmation-message');
+ const confirmBtn = document.getElementById('confirm-action');
+ const cancelBtn = document.getElementById('cancel-action');
+
+ titleEl.textContent = title;
+ messageEl.textContent = message;
+
+ dialog.classList.remove('hidden');
+ setTimeout(() => dialog.classList.add('active'), 10);
+
+ const closeDialog = () => {
+ dialog.classList.remove('active');
+ setTimeout(() => dialog.classList.add('hidden'), 300);
+ };
+
+ const handleConfirm = () => {
+ onConfirm();
+ closeDialog();
+ confirmBtn.removeEventListener('click', handleConfirm);
+ cancelBtn.removeEventListener('click', handleCancel);
+ };
+
+ const handleCancel = () => {
+ closeDialog();
+ confirmBtn.removeEventListener('click', handleConfirm);
+ cancelBtn.removeEventListener('click', handleCancel);
+ };
+
+ confirmBtn.addEventListener('click', handleConfirm);
+ cancelBtn.addEventListener('click', handleCancel);
+ },
+
+ showPasswordDialog: (shortcut, callback) => {
+ const dialog = document.getElementById('password-dialog');
+ const passwordInput = document.getElementById('shortcut-password');
+ const submitBtn = document.getElementById('submit-password');
+ const cancelBtn = document.getElementById('cancel-password');
+ const closeBtn = document.getElementById('close-password-dialog');
+ const errorMsg = document.getElementById('password-error');
+ const contextMenu = document.getElementById('context-menu');
+
+ if (contextMenu) {
+ contextMenu.classList.add('hidden');
+ }
+
+ if (errorMsg) {
+ errorMsg.classList.add('hidden');
+ }
+
+ if (passwordInput) {
+ passwordInput.value = '';
+ }
+
+ if (dialog) {
+ dialog.classList.remove('hidden');
+ setTimeout(() => {
+ dialog.classList.add('active');
+ if (passwordInput) {
+ passwordInput.focus();
+ }
+ }, 10);
+ }
+
+ const closeDialog = () => {
+ dialog.classList.remove('active');
+ setTimeout(() => dialog.classList.add('hidden'), 300);
+ };
+
+ const handleSubmit = () => {
+ const password = passwordInput.value;
+ const masterPassword = Storage.get('masterPassword');
+
+ if (!masterPassword) {
+ errorMsg.textContent = "No master password set. Please set one in settings.";
+ errorMsg.classList.remove('hidden');
+ return;
+ }
+
+ if (password === masterPassword) {
+ closeDialog();
+ callback();
+ submitBtn.removeEventListener('click', handleSubmit);
+ cancelBtn.removeEventListener('click', handleCancel);
+ closeBtn.removeEventListener('click', handleCancel);
+ passwordInput.removeEventListener('keydown', handleKeydown);
+ } else {
+ errorMsg.textContent = "Incorrect password. Please try again.";
+ errorMsg.classList.remove('hidden');
+ passwordInput.value = '';
+ passwordInput.focus();
+ }
+ };
+
+ const handleCancel = () => {
+ closeDialog();
+ submitBtn.removeEventListener('click', handleSubmit);
+ cancelBtn.removeEventListener('click', handleCancel);
+ closeBtn.removeEventListener('click', handleCancel);
+ passwordInput.removeEventListener('keydown', handleKeydown);
+ };
+
+ const handleKeydown = (e) => {
+ if (e.key === 'Enter') {
+ handleSubmit();
+ } else if (e.key === 'Escape') {
+ handleCancel();
+ }
+ };
+
+ submitBtn.addEventListener('click', handleSubmit);
+ cancelBtn.addEventListener('click', handleCancel);
+ closeBtn.addEventListener('click', handleCancel);
+ passwordInput.addEventListener('keydown', handleKeydown);
+ },
+
+ edit: (index, newUrl, newName, isPasswordProtected) => {
const currentShortcuts = Storage.get('shortcuts') || [];
- currentShortcuts[index] = { url: newUrl, name: newName };
+ currentShortcuts[index] = {
+ url: newUrl,
+ name: newName,
+ isPasswordProtected: isPasswordProtected || false
+ };
Storage.set('shortcuts', currentShortcuts);
shortcuts.render();
notifications.show('Shortcut updated!', 'success');
@@ -82,7 +208,7 @@ const shortcuts = {
currentShortcuts.forEach((shortcut, index) => {
const element = document.createElement('div');
- element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`;
+ element.className = `shortcut ${isAnonymous ? 'blurred' : ''} ${shortcut.isPasswordProtected ? 'password-protected' : ''}`;
element.dataset.index = index;
@@ -99,10 +225,42 @@ const shortcuts = {
element.addEventListener('click', (e) => {
if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) {
- if (e.ctrlKey) {
+ if (shortcut.isPasswordProtected) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const openShortcut = () => {
+ if (e.ctrlKey || e.which === 2 || e.button === 1) {
+ window.open(shortcut.url, '_blank');
+ } else {
+ window.location.href = shortcut.url;
+ }
+ };
+
+ shortcuts.showPasswordDialog(shortcut, openShortcut);
+ return false;
+ } else {
+ if (e.ctrlKey || e.which === 2 || e.button === 1) {
window.open(shortcut.url, '_blank');
} else {
window.location.href = shortcut.url;
+ }
+ }
+ }
+ });
+
+ element.addEventListener('mousedown', (e) => {
+ if (e.button === 1) {
+ e.preventDefault();
+
+ if (shortcut.isPasswordProtected) {
+ const openShortcut = () => {
+ window.open(shortcut.url, '_blank');
+ };
+
+ shortcuts.showPasswordDialog(shortcut, openShortcut);
+ } else {
+ window.open(shortcut.url, '_blank');
}
}
});
@@ -133,6 +291,14 @@ const shortcuts = {
},
init: () => {
+ const masterPasswordInput = document.getElementById('master-password');
+ if (masterPasswordInput) {
+ const savedPassword = Storage.get('masterPassword');
+ if (savedPassword) {
+ masterPasswordInput.value = savedPassword;
+ }
+ }
+
const addShortcutButton = document.getElementById('add-shortcut');
const modal = document.getElementById('add-shortcut-modal');
const closeBtn = modal.querySelector('.close-modal');
@@ -174,12 +340,18 @@ const shortcuts = {
if (url && name) {
try {
new URL(url);
- shortcuts.add(url, name);
+
+ const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false;
+ const passwordProtectCheckbox = document.getElementById('protect-shortcut');
+ const isPasswordProtected = isPasswordProtectionEnabled && passwordProtectCheckbox && passwordProtectCheckbox.checked;
+
+ shortcuts.add(url, name, isPasswordProtected);
modal.classList.remove('active');
setTimeout(() => {
modal.classList.add('hidden');
urlInput.value = '';
nameInput.value = '';
+ if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false;
}, 300);
notifications.show('Shortcut added successfully!', 'success');
} catch (e) {
@@ -197,6 +369,8 @@ const shortcuts = {
modal.classList.add('hidden');
urlInput.value = '';
nameInput.value = '';
+ const passwordProtectCheckbox = document.getElementById('protect-shortcut');
+ if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false;
}, 300);
};
}
@@ -204,6 +378,121 @@ const shortcuts = {
});
}
+ const anonymousTogglePrivacy = document.getElementById('toggle-anonymous-privacy');
+ if (anonymousTogglePrivacy) {
+ anonymousTogglePrivacy.checked = Storage.get('anonymousMode') || false;
+
+ anonymousTogglePrivacy.addEventListener('change', () => {
+ const anonymousToggle = document.getElementById('toggle-anonymous');
+ if (anonymousToggle) {
+ anonymousToggle.checked = anonymousTogglePrivacy.checked;
+ anonymousToggle.dispatchEvent(new Event('change'));
+ } else {
+ shortcuts.toggleAnonymousMode();
+ }
+ });
+ }
+
+ const shortcutsPasswordToggle = document.getElementById('toggle-shortcuts-password');
+ if (shortcutsPasswordToggle) {
+ const isEnabled = Storage.get('passwordProtectionEnabled') || false;
+ shortcutsPasswordToggle.checked = isEnabled;
+ const passwordSettings = document.getElementById('password-protection-settings');
+
+ if (passwordSettings) {
+ if (isEnabled) {
+ passwordSettings.classList.remove('hidden');
+ } else {
+ passwordSettings.classList.add('hidden');
+ }
+ }
+
+ shortcutsPasswordToggle.addEventListener('change', () => {
+ const isEnabled = shortcutsPasswordToggle.checked;
+ Storage.set('passwordProtectionEnabled', isEnabled);
+
+ if (passwordSettings) {
+ if (isEnabled) {
+ passwordSettings.classList.remove('hidden');
+ } else {
+ passwordSettings.classList.add('hidden');
+ }
+ }
+
+ shortcuts.updateAddShortcutModal();
+
+ if (isEnabled) {
+ const masterPassword = Storage.get('masterPassword');
+
+ if (!masterPassword) {
+ const masterPasswordInput = document.getElementById('master-password');
+ if (masterPasswordInput) {
+ masterPasswordInput.focus();
+ notifications.show('Please set a master password!', 'warning');
+ }
+ } else {
+ notifications.show('Password protection enabled!', 'success');
+
+ setTimeout(() => {
+ shortcuts.createShortcutProtectionManager();
+ }, 10);
+ }
+ } else {
+ const currentShortcuts = Storage.get('shortcuts') || [];
+ currentShortcuts.forEach(shortcut => {
+ shortcut.isPasswordProtected = false;
+ });
+ Storage.set('shortcuts', currentShortcuts);
+ shortcuts.render();
+
+ notifications.show('Password protection disabled!', 'info');
+ }
+ });
+ }
+
+ if (Storage.get('passwordProtectionEnabled')) {
+ shortcuts.createShortcutProtectionManager();
+ }
+
+ const saveMasterPasswordBtn = document.getElementById('save-master-password');
+ if (saveMasterPasswordBtn) {
+ saveMasterPasswordBtn.addEventListener('click', () => {
+ const masterPasswordInput = document.getElementById('master-password');
+ if (masterPasswordInput) {
+ const password = masterPasswordInput.value.trim();
+
+ if (password) {
+ Storage.set('masterPassword', password);
+
+ notifications.show('Master password updated!', 'success');
+
+ const shortcutsPasswordToggle = document.getElementById('toggle-shortcuts-password');
+ if (shortcutsPasswordToggle && !shortcutsPasswordToggle.checked) {
+ shortcutsPasswordToggle.checked = true;
+ Storage.set('passwordProtectionEnabled', true);
+ const passwordSettings = document.getElementById('password-protection-settings');
+ if (passwordSettings) {
+ passwordSettings.classList.remove('hidden');
+ }
+ shortcuts.updateAddShortcutModal();
+
+ setTimeout(() => {
+ shortcuts.createShortcutProtectionManager();
+ }, 10);
+ } else {
+ setTimeout(() => {
+ shortcuts.createShortcutProtectionManager();
+ }, 10);
+ }
+ } else {
+ notifications.show('Please enter a valid password!', 'error');
+ }
+ }
+ });
+ }
+
+ shortcuts.updateAddShortcutModal();
+
const contextMenu = document.getElementById('context-menu');
if (contextMenu) {
contextMenu.addEventListener('click', (e) => {
@@ -222,6 +511,11 @@ const shortcuts = {
urlInput.value = shortcut.url;
nameInput.value = shortcut.name;
+ const protectCheckbox = document.getElementById('protect-shortcut-edit');
+ if (protectCheckbox) {
+ protectCheckbox.checked = shortcut.isPasswordProtected || false;
+ }
+
modal.classList.remove('hidden');
modal.classList.add('active');
@@ -243,8 +537,14 @@ const shortcuts = {
if (newUrl && newName) {
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl);
if (formattedUrl) {
- shortcuts.edit(index, formattedUrl, newName);
+ const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false;
+ const isPasswordProtected = isPasswordProtectionEnabled &&
+ protectCheckbox && protectCheckbox.checked;
+
+ shortcuts.edit(index, formattedUrl, newName, isPasswordProtected);
closeModal();
+
+ shortcuts.createShortcutProtectionManager();
} else {
notifications.show('Invalid URL format!', 'error');
}
@@ -256,13 +556,29 @@ const shortcuts = {
cancelButton.onclick = closeModal;
}
} else if (action === 'delete') {
+ const currentShortcuts = Storage.get('shortcuts') || [];
+ const shortcut = currentShortcuts[index];
+
+ shortcuts.showConfirmDialog(
+ 'Delete Shortcut',
+ `Are you sure you want to delete "${shortcut.name}"?`,
+ () => {
shortcuts.remove(index);
+ shortcuts.createShortcutProtectionManager();
+ }
+ );
} else if (action === 'open-new-tab') {
const currentShortcuts = Storage.get('shortcuts') || [];
const shortcut = currentShortcuts[index];
if (shortcut && shortcut.url) {
+ if (shortcut.isPasswordProtected) {
+ shortcuts.showPasswordDialog(shortcut, () => {
+ window.open(shortcut.url, '_blank');
+ });
+ } else {
window.open(shortcut.url, '_blank');
+ }
}
}
@@ -271,5 +587,240 @@ const shortcuts = {
}
shortcuts.render();
+ },
+
+ createShortcutProtectionManager: () => {
+ const passwordSettings = document.getElementById('password-protection-settings');
+ if (!passwordSettings) return;
+
+ let protectionManager = document.getElementById('shortcut-protection-manager');
+ if (!protectionManager) {
+ protectionManager = document.createElement('div');
+ protectionManager.id = 'shortcut-protection-manager';
+ protectionManager.className = 'shortcut-protection-manager';
+
+ const managerTitle = document.createElement('h4');
+ managerTitle.textContent = 'Protect Specific Shortcuts';
+
+ const managerDescription = document.createElement('p');
+ managerDescription.className = 'setting-description';
+ managerDescription.textContent = 'Select which shortcuts to password protect:';
+
+ protectionManager.appendChild(managerTitle);
+ protectionManager.appendChild(managerDescription);
+
+ passwordSettings.appendChild(protectionManager);
+ } else {
+ const children = Array.from(protectionManager.children);
+ children.forEach((child, index) => {
+ if (index > 1) protectionManager.removeChild(child);
+ });
+ }
+
+ const currentShortcuts = Storage.get('shortcuts') || [];
+
+ const selectedShortcutsContainer = document.createElement('div');
+ selectedShortcutsContainer.className = 'selected-shortcuts-container';
+
+ const protectedShortcuts = currentShortcuts.filter(shortcut => shortcut.isPasswordProtected);
+
+ if (protectedShortcuts.length > 0) {
+ protectedShortcuts.forEach((shortcut, index) => {
+ const shortcutChip = document.createElement('div');
+ shortcutChip.className = 'shortcut-chip';
+ shortcutChip.dataset.index = currentShortcuts.indexOf(shortcut);
+
+ const shortcutIcon = document.createElement('img');
+ shortcutIcon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
+ shortcutIcon.alt = shortcut.name;
+
+ const shortcutName = document.createElement('span');
+ shortcutName.textContent = shortcut.name;
+
+ const removeButton = document.createElement('button');
+ removeButton.className = 'remove-chip-btn';
+ removeButton.innerHTML = '×';
+ removeButton.title = 'Remove protection';
+
+ shortcutChip.appendChild(shortcutIcon);
+ shortcutChip.appendChild(shortcutName);
+ shortcutChip.appendChild(removeButton);
+
+ removeButton.addEventListener('click', (e) => {
+ e.stopPropagation();
+ currentShortcuts[shortcutChip.dataset.index].isPasswordProtected = false;
+ Storage.set('shortcuts', currentShortcuts);
+
+ shortcuts.render();
+ shortcuts.createShortcutProtectionManager();
+
+ notifications.show(`Removed protection from: ${shortcut.name}`, 'info');
+ });
+
+ selectedShortcutsContainer.appendChild(shortcutChip);
+ });
+ } else if (currentShortcuts.length > 0) {
+ const emptyState = document.createElement('p');
+ emptyState.className = 'empty-protection-state';
+ emptyState.textContent = 'No protected shortcuts yet.';
+ selectedShortcutsContainer.appendChild(emptyState);
+ }
+
+ protectionManager.appendChild(selectedShortcutsContainer);
+
+ const selectorContainer = document.createElement('div');
+ selectorContainer.className = 'shortcut-selector-container';
+
+ const unprotectedShortcuts = currentShortcuts.filter(shortcut => !shortcut.isPasswordProtected);
+
+ if (unprotectedShortcuts.length > 0) {
+ const dropdown = document.createElement('div');
+ dropdown.className = 'shortcut-dropdown';
+
+ const selected = document.createElement('div');
+ selected.className = 'shortcut-dropdown-selected';
+ selected.textContent = 'Select a shortcut to protect...';
+
+ const dropdownItems = document.createElement('div');
+ dropdownItems.className = 'shortcut-dropdown-items';
+ dropdownItems.classList.add('hidden');
+
+ unprotectedShortcuts.forEach(shortcut => {
+ const item = document.createElement('div');
+ item.className = 'shortcut-dropdown-item';
+ item.dataset.index = currentShortcuts.indexOf(shortcut);
+
+ const icon = document.createElement('img');
+ icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
+ icon.alt = shortcut.name;
+ icon.style.width = '16px';
+ icon.style.height = '16px';
+
+ const name = document.createElement('span');
+ name.textContent = shortcut.name;
+
+ item.appendChild(icon);
+ item.appendChild(name);
+
+ item.addEventListener('click', () => {
+ currentShortcuts[item.dataset.index].isPasswordProtected = true;
+ Storage.set('shortcuts', currentShortcuts);
+
+ shortcuts.render();
+ shortcuts.createShortcutProtectionManager();
+
+ dropdownItems.classList.remove('active');
+ selected.classList.remove('active');
+ dropdownItems.classList.add('hidden');
+
+ notifications.show(`Protected shortcut: ${shortcut.name}`, 'success');
+ });
+
+ dropdownItems.appendChild(item);
+ });
+
+ selected.addEventListener('click', (e) => {
+ e.stopPropagation();
+ dropdownItems.classList.toggle('hidden');
+ dropdownItems.classList.toggle('active');
+ selected.classList.toggle('active');
+ });
+
+ document.addEventListener('click', (e) => {
+ if (!dropdown.contains(e.target)) {
+ dropdownItems.classList.add('hidden');
+ dropdownItems.classList.remove('active');
+ selected.classList.remove('active');
+ }
+ });
+
+ dropdown.appendChild(selected);
+ dropdown.appendChild(dropdownItems);
+ selectorContainer.appendChild(dropdown);
+ } else if (currentShortcuts.length === 0) {
+ const noShortcutsMessage = document.createElement('p');
+ noShortcutsMessage.className = 'no-shortcuts-message';
+ noShortcutsMessage.textContent = 'Add shortcuts to protect them with a password.';
+ selectorContainer.appendChild(noShortcutsMessage);
+ } else {
+ const allProtectedMessage = document.createElement('p');
+ allProtectedMessage.className = 'empty-protection-state';
+ allProtectedMessage.textContent = 'All shortcuts are password protected.';
+ selectorContainer.appendChild(allProtectedMessage);
+ }
+
+ protectionManager.appendChild(selectorContainer);
+ },
+
+ updateAddShortcutModal: () => {
+ const modal = document.getElementById('add-shortcut-modal');
+ if (!modal) return;
+
+ const modalContent = modal.querySelector('.modal-content');
+ if (!modalContent) return;
+
+ const existingCheckbox = document.getElementById('protect-shortcut-container');
+ if (existingCheckbox) return;
+
+ const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false;
+ if (!isPasswordProtectionEnabled) return;
+
+ const checkboxContainer = document.createElement('div');
+ checkboxContainer.id = 'protect-shortcut-container';
+ checkboxContainer.className = 'checkbox-container';
+ checkboxContainer.innerHTML = `
+
+ `;
+
+ const saveButton = modal.querySelector('#save-shortcut');
+ if (saveButton) {
+ modalContent.insertBefore(checkboxContainer, saveButton);
+ } else {
+ modalContent.appendChild(checkboxContainer);
+ }
+
+ const editModal = document.getElementById('edit-shortcut-modal');
+ if (editModal) {
+ const existingEditCheckbox = document.getElementById('protect-shortcut-edit-container');
+ if (!existingEditCheckbox) {
+ const editModalContent = editModal.querySelector('.modal-content');
+ const editCheckboxContainer = document.createElement('div');
+ editCheckboxContainer.id = 'protect-shortcut-edit-container';
+ editCheckboxContainer.className = 'checkbox-container';
+ editCheckboxContainer.innerHTML = `
+
+ `;
+
+ const modalActions = editModal.querySelector('.modal-actions');
+ if (modalActions) {
+ editModalContent.insertBefore(editCheckboxContainer, modalActions);
+ } else {
+ editModalContent.appendChild(editCheckboxContainer);
+ }
+ }
+ }
+ },
+
+ toggleAnonymousMode: () => {
+ const isAnonymous = Storage.get('anonymousMode') || false;
+ Storage.set('anonymousMode', !isAnonymous);
+
+ if (!isAnonymous) {
+ const randomName = anonymousNames.generate();
+ Storage.set('anonymousName', randomName);
+ notifications.show('Anonymous mode enabled!', 'info');
+ } else {
+ Storage.remove('anonymousName');
+ notifications.show('Anonymous mode disabled!', 'info');
+ }
+
+ shortcuts.render();
+ updateGreeting();
}
};
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index b714972..a6fe4ed 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,35 +1,36 @@
-{
- "manifest_version": 3,
- "name": "JSTAR Tab",
- "version": "3.0.0",
- "description": "JSTAR Tab is a sleek, customizable new tab extension with personalized greetings, shortcuts, anonymous mode, search engine settings, themes, data management, and more, for an enhanced browsing experience.",
- "chrome_url_overrides": {
- "newtab": "index.html"
- },
- "permissions": [
- "storage",
- "favicon"
- ],
- "icons": {
- "16": "images/icon16.png",
- "48": "images/icon48.png",
- "128": "images/icon128.png"
- },
- "action": {
- "default_title": "New JSTAR Tab",
- "default_icon": {
- "16": "images/icon16.png",
- "48": "images/icon48.png",
- "128": "images/icon128.png"
- }
- },
- "author": "JSTAR",
- "homepage_url": "https://github.com/DevJSTAR/JSTAR-Tab",
- "web_accessible_resources": [{
- "resources": [
- "fonts/*",
- "images/*"
- ],
- "matches": ["
"]
- }]
+{
+ "manifest_version": 3,
+ "name": "JSTAR Tab",
+ "version": "3.2.0",
+ "description": "JSTAR Tab is a sleek, customizable new tab extension with personalized greetings, shortcuts, anonymous mode, search engine settings, themes, data management, and more, for an enhanced browsing experience.",
+ "chrome_url_overrides": {
+ "newtab": "index.html"
+ },
+ "permissions": [
+ "storage",
+ "favicon",
+ "history"
+ ],
+ "icons": {
+ "16": "images/icon16.png",
+ "48": "images/icon48.png",
+ "128": "images/icon128.png"
+ },
+ "action": {
+ "default_title": "New JSTAR Tab",
+ "default_icon": {
+ "16": "images/icon16.png",
+ "48": "images/icon48.png",
+ "128": "images/icon128.png"
+ }
+ },
+ "author": "JSTAR",
+ "homepage_url": "https://github.com/DevJSTAR/JSTAR-Tab",
+ "web_accessible_resources": [{
+ "resources": [
+ "fonts/*",
+ "images/*"
+ ],
+ "matches": [""]
+ }]
}
\ No newline at end of file