Compare commits

..

53 Commits
1.0.0 ... main

Author SHA1 Message Date
JSTAR
9d9305b69a
Add files via upload 2025-04-18 22:19:06 +05:00
JSTAR
ac0a56c08e
Update README.md 2025-04-18 21:51:26 +05:00
JSTAR
f2b3a43135
Add files via upload 2025-04-18 21:35:21 +05:00
JSTAR
7cc5e4010e
Update README.md 2025-03-23 23:50:17 +05:00
JSTAR
46ef89458d
Delete style.css 2025-03-23 23:34:19 +05:00
JSTAR
e8b5dba408
Add files via upload 2025-03-23 23:34:00 +05:00
JSTAR
293a1e9e8a
Delete images/favicon.ico 2025-03-23 23:30:09 +05:00
JSTAR
1b4cbc6e27
Delete images/icon16.png 2025-03-23 23:30:01 +05:00
JSTAR
196051f263
Delete images/icon48.png 2025-03-23 23:29:51 +05:00
JSTAR
76c663c0bf
Delete images/icon128.png 2025-03-23 23:29:38 +05:00
JSTAR
3e6b86b79d
Update LICENSE 2025-02-23 14:08:35 +05:00
JSTAR
683f6b7532
Update README.md 2024-12-15 22:43:59 +05:00
JSTAR
56fe2fd9f2
Update README.md 2024-12-15 22:42:58 +05:00
JSTAR
ab9d9a1849
Add files via upload 2024-12-14 15:19:39 +05:00
JSTAR
ed9aa01ea0
Add files via upload 2024-12-14 15:03:57 +05:00
JSTAR
5c6dc07bb4
Update index.html 2024-12-14 14:54:51 +05:00
JSTAR
6e076575f2
Update README.md 2024-12-14 14:52:27 +05:00
JSTAR
99b7538de4
Update style.css 2024-12-14 14:41:18 +05:00
JSTAR
659ca86f17
Update index.html 2024-12-14 14:38:10 +05:00
JSTAR
1b7211317a
Merge pull request #2 from EquaTechnologies/main
JSTAR Tab v2.6.2
2024-12-14 14:37:16 +05:00
Equa
87f8e0ff11
Delete version.json (not needed file) 2024-12-13 21:39:00 +02:00
Equa
8a326c7cdb
Update shortcuts.js 2024-12-13 21:18:26 +02:00
Equa
a872db13a2
Update index.html 2024-12-13 21:06:46 +02:00
Equa
787413d027
Update style.css 2024-12-13 21:02:08 +02:00
Equa
1b67360deb
Update style.css 2024-12-13 20:58:04 +02:00
Equa
a409389215
Update style.css 2024-12-13 20:23:09 +02:00
Equa
888580b342
Update style.css 2024-12-13 20:08:54 +02:00
Equa
6b0735351d
Update manifest.json 2024-12-13 20:04:16 +02:00
Equa
66b70330b9
Update version.json 2024-12-13 20:03:44 +02:00
JSTAR
1faabe03c7
Delete version.json 2024-12-13 23:02:58 +05:00
Equa
a7f7b07377
Update style.css button style 2024-12-13 20:00:35 +02:00
Equa
2074b0db58
Update README.md
skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma skibidi sigma
2024-12-13 19:55:34 +02:00
JSTAR
350e65ff29
Create version.json 2024-12-13 00:00:57 +05:00
JSTAR
49f25d3405
Add files via upload 2024-11-12 18:45:24 +05:00
JSTAR
5a8ed20b50
Release of v2.6.0 2024-11-12 18:44:08 +05:00
JSTAR
dfe7ec0ac7
Update index.html 2024-11-12 01:47:00 +05:00
JSTAR
e09a542bfd
Add files via upload 2024-11-11 21:46:25 +05:00
JSTAR
d09a09f90e
Delete js directory 2024-11-11 21:45:34 +05:00
JSTAR
8f341d1c7e
Delete style.css 2024-11-11 21:45:22 +05:00
JSTAR
aa02538761
Delete manifest.json 2024-11-11 21:45:11 +05:00
JSTAR
3ebdef5fe8
Delete index.html 2024-11-11 21:45:01 +05:00
JSTAR
5daae99924
Update README.md 2024-11-11 21:44:40 +05:00
JSTAR
70fe00ebb0
Update README.md 2024-11-11 15:42:53 +05:00
JSTAR
04c3b56227
Update index.html 2024-11-10 23:06:48 +05:00
JSTAR
60995a0d03
Add files via upload 2024-11-10 23:00:53 +05:00
JSTAR
5de11f7dc4
Delete js directory 2024-11-10 22:53:21 +05:00
JSTAR
3d6853d8b3
Delete index.html 2024-11-10 22:53:08 +05:00
JSTAR
3a1ea2ead6
Update README.md 2024-11-10 22:43:52 +05:00
JSTAR
fed70824e1
Add files via upload 2024-11-10 22:31:43 +05:00
JSTAR
248e7eb26c
Delete style.css 2024-11-10 22:30:18 +05:00
JSTAR
7d91b317c7
Delete script.js 2024-11-10 22:30:05 +05:00
JSTAR
ef727dabb8
Delete index.html 2024-11-10 22:29:56 +05:00
JSTAR
afcc635995
Delete manifest.json 2024-11-10 22:29:45 +05:00
33 changed files with 9858 additions and 946 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 JSTAR
Copyright (c) 2025 JSTAR
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

100
README.md
View File

@ -1,58 +1,84 @@
# 🌟 JSTAR Tab v1.0.0
# 🌟 JSTAR Tab
Welcome to **JSTAR Tab v1.0.0**! This extension transforms your new tab into a personalized dashboard with shortcuts, a search bar, and customizable settings. 🚀
Welcome to **JSTAR Tab**, the ultimate customizable new tab extension for your browser! Whether you're looking for a sleek, modern design, powerful personalization options, or enhanced accessibility, JSTAR Tab has you covered. 🚀
## 📥 Installation
Transform your browsing experience with custom greetings, motion control, themes, fonts, and more. 🎉
To set up JSTAR Tab locally, follow these steps:
## ✨ Features
1. **Download the Files**: Clone or download the repository to your local machine.
- **Custom Greeting Formats**: Create personalized greetings using dynamic format tags:
- `{name}`: Your display name
- `{greeting}`: Time-based greeting (e.g., Good morning)
- `{time}`: Current time
- `{date}`: Current date
- `{day}`: Day of the week
- `{month}`: Current month
- `{year}`: Current year
Examples:
- "Hello {name}, it's {time} on {day}!"
- "{greeting}, {name}! Today is {date}"
- "Happy {day}, {name}!"
- **Font Selection**: Choose from multiple fonts (Inter, Poppins, Roboto, Montserrat, Quicksand, and Comic Sans) to personalize your experience.
- **Custom Background Images**: Upload and set your favorite images as your new tab background.
- **Customizable Themes**: Switch between light and dark modes to suit your mood.
- **Motion Preferences**: Control animation intensity across five levels: Default Animations, Subtle Animations, Reduced Motion, Minimal Motion, and No Animations. Perfect for accessibility and performance preferences.
- **Privacy Settings**: Includes Anonymous Mode, password protection for individual shortcuts, and more to give you control over your experience.
- **Shortcut Management**: Add, edit, and remove shortcuts to your favorite websites with an improved grid layout system. Middle-click support allows opening shortcuts in a new tab.
- **History Page**: View and manage your shortcut browsing history from a dedicated page.
- **Search Engine Selection**: Choose your preferred search engine for quick and efficient browsing.
- **Keyboard Shortcuts**: Set up custom keybinds for various actions:
- Open settings
- Toggle anonymous mode
- Change themes
- Open history
- Redirect to a specific URL
- **Data Backup and Restore**: Export and import your settings and shortcuts effortlessly.
## 🌐 Getting Started
### Installation
1. Visit [JSTAR Tab Website](https://jstartab.netlify.app).
2. Select your browser or mobile browser from the available options.
3. Follow the installation instructions specific to your browser.
### Alternative Manual Installation
1. **Download the Latest Release**: [JSTAR Tab GitHub Releases](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest).
2. **Extract the Files**: Unzip the downloaded file to a directory of your choice.
3. **Load the Extension**:
- Open your browser and navigate to the extensions page.
- Enable "Developer mode" (usually a toggle in the top right corner).
- Click "Load unpacked" and select the directory where you extracted the files.
## 🛠️ Features
## 🎨 Customizing Your Experience
- **Customizable Themes**: Switch between light and dark themes to suit your mood. 🌗
- **Personalized Greeting**: Displays a greeting based on the time of day and your anonymization settings.
- **Search Bar**: Quickly search the web using Brave's search engine.
- **Shortcut Management**: Add, edit, and remove shortcuts to your favorite sites.
- **Settings Panel**: Access various settings including theme toggle and shortcut management.
- **Anonymization**: Toggle anonymization to hide your name in the greeting.
### **Greeting Formats**
Personalize your greeting with dynamic tags listed above. Example:
"Good {greeting}, {name}! It's {time} on {day}!"
## 🎨 Customizing Your JSTAR Tab
### **Themes & Fonts**
- Toggle between light and dark themes from the settings panel.
- Select from various font options to match your style.
1. **Themes**: Use the theme switch in the settings to toggle between light and dark modes.
### **Shortcuts & Backgrounds**
- Add shortcuts with the "+" button.
- Edit or delete shortcuts by right-clicking on them.
- Customize your background by uploading your favorite images.
2. **Greeting**: The greeting changes based on the time of day and whether anonymization is enabled.
3. **Shortcuts**:
- **Add**: Click the "Add Shortcut" button, enter a name and URL, and save.
- **Edit**: Right-click on a shortcut and select "Edit" to modify its details.
- **Delete**: Right-click on a shortcut and select "Delete" to remove it.
4. **Search Engine**: The search bar uses Brave's search engine by default.
## 📚 Usage
- **Settings**: Click the settings icon ⚙️ to open the settings panel where you can customize your experience.
- **Add Shortcuts**: Click the "+" button to add a new shortcut. Enter the URL and name, then save.
- **Edit Shortcuts**: Right-click on a shortcut to edit or delete it.
- **Theme Toggle**: Switch between light and dark themes using the theme toggle button.
## 📝 Contributing
We welcome contributions! If you have suggestions or improvements, feel free to fork the repository and submit a pull request. For major changes, please open an issue first to discuss what you would like to change.
### **Keyboard Shortcuts**
Set up custom keybinds for quick actions like opening settings or switching themes.
## 📄 License
This project is licensed under the [MIT License](https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE).
## ❤️ Acknowledgments
## 🌐 Connect with Us
- **Font Awesome**: For the beautiful icons used throughout the extension.
- **Google Fonts**: For the sleek and modern typography.
- **[Linktree](https://linktr.ee/jstarsdev)**
- **[Patreon](https://patreon.com/jstarsdev)**
- **[GitHub Releases](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest)**
Thank you for using JSTAR Tab! We hope it makes your browsing experience more enjoyable and productive. 😊
Thank you for choosing JSTAR Tab! We hope you enjoy the seamless, personalized, and now even more **accessible** browsing experience it brings. 🚀

567
css/history.css Normal file
View File

@ -0,0 +1,567 @@
:root {
--history-background: var(--card-bg, #ffffff);
--history-text: var(--text-color, #333333);
--history-accent: var(--accent-color, #4a6cf7);
--history-hover: var(--hover-bg, #f5f7ff);
--history-border: var(--border-color, #eaeaea);
--history-danger: #e53935;
--history-danger-hover: #c62828;
--history-success: #43a047;
--history-muted: #757575;
--history-header-height: 120px;
--history-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
--history-radius: 8px;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
font-family: var(--font-family, 'Inter', sans-serif);
overflow-x: hidden;
}
.history-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--history-background);
color: var(--history-text);
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.history-header {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--history-background);
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 0;
border-bottom: 1px solid var(--history-border);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-left h1 {
font-size: 24px;
font-weight: 600;
margin: 0;
}
.header-right {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.back-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: var(--history-hover);
cursor: pointer;
transition: background 0.2s;
}
.back-button:hover {
background: var(--history-border);
}
.back-button svg {
width: 20px;
height: 20px;
stroke: var(--history-text);
}
.search-bar {
display: flex;
align-items: center;
background-color: var(--history-hover);
border-radius: 24px;
padding: 8px 16px;
flex: 1;
max-width: 500px;
position: relative;
height: 40px;
}
.search-bar svg {
width: 18px;
height: 18px;
margin-right: 8px;
stroke: var(--history-muted);
}
.search-bar input {
flex: 1;
border: none;
background: transparent;
font-size: 14px;
outline: none;
color: var(--history-text);
}
.search-bar input::placeholder {
color: var(--history-muted);
}
.search-clear {
background: transparent;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 4px;
border-radius: 50%;
}
.search-clear svg {
width: 16px;
height: 16px;
margin: 0;
}
.search-clear:hover {
background: rgba(0, 0, 0, 0.05);
}
.filter-container {
position: relative;
}
.filter-container select {
appearance: none;
background-color: var(--history-hover);
border: none;
border-radius: 24px;
padding: 8px 16px;
font-size: 14px;
color: var(--history-text);
cursor: pointer;
outline: none;
min-width: 140px;
height: 40px;
}
.filter-container::after {
content: "";
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid var(--history-text);
pointer-events: none;
}
.history-actions {
display: flex;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--history-border);
}
.history-actions button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 24px;
border: 1px solid var(--history-border);
background: transparent;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.history-actions button svg {
width: 16px;
height: 16px;
stroke: currentColor;
}
.history-actions button:hover {
background: var(--history-hover);
}
.history-actions button:active {
transform: translateY(1px);
}
.history-actions button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
#clear-all {
color: var(--history-danger);
}
#clear-all:hover {
background: rgba(229, 57, 53, 0.1);
}
.history-content {
flex: 1;
overflow-y: auto;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
}
.history-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
gap: 16px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--history-border);
border-top-color: var(--history-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.history-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
}
.history-empty .empty-icon {
width: 80px;
height: 80px;
margin-bottom: 16px;
color: var(--history-muted);
display: flex;
justify-content: center;
align-items: center;
}
.history-empty .empty-icon svg {
width: 100%;
height: 100%;
}
.history-empty h2 {
font-size: 20px;
font-weight: 600;
margin: 0 0 8px;
}
.history-empty p {
color: var(--history-muted);
max-width: 300px;
}
.history-items {
display: flex;
flex-direction: column;
gap: 20px;
}
.history-date-group {
display: flex;
flex-direction: column;
}
.history-date-header {
font-size: 14px;
font-weight: 600;
color: var(--history-muted);
padding: 8px 0;
border-bottom: 1px solid var(--history-border);
margin-bottom: 8px;
}
.history-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: var(--history-radius);
transition: background 0.2s;
position: relative;
}
.history-item:hover {
background: var(--history-hover);
}
.history-item-checkbox {
margin-right: 12px;
}
.history-item-favicon {
width: 20px;
height: 20px;
margin-right: 12px;
border-radius: 4px;
background: var(--history-border);
overflow: hidden;
}
.history-item-favicon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.history-item-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.history-item-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item-url {
font-size: 12px;
color: var(--history-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item-time {
font-size: 12px;
color: var(--history-muted);
margin-left: 16px;
white-space: nowrap;
min-width: 70px;
text-align: right;
}
.history-item-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
opacity: 0;
transition: opacity 0.2s;
}
.history-item:hover .history-item-actions {
opacity: 1;
}
.history-item-action {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
background: transparent;
cursor: pointer;
}
.history-item-action:hover {
background: rgba(0, 0, 0, 0.05);
}
.history-item-action svg {
width: 16px;
height: 16px;
stroke: var(--history-muted);
}
.history-item-action.delete:hover svg {
stroke: var(--history-danger);
}
.history-load-more {
display: flex;
justify-content: center;
padding: 20px 0;
}
.history-load-more button {
padding: 8px 24px;
border-radius: 24px;
border: none;
background: var(--history-accent);
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.history-load-more button:hover {
background: var(--history-accent);
filter: brightness(1.1);
}
.hidden {
display: none !important;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
}
.modal.hidden {
display: none;
}
.modal:not(.hidden) {
opacity: 1;
visibility: visible;
}
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: var(--history-background);
border-radius: var(--history-radius);
box-shadow: var(--history-shadow);
position: relative;
z-index: 1;
transform: translateY(20px);
transition: transform 0.3s;
max-width: 90%;
}
.modal:not(.hidden) .modal-content {
transform: translateY(0);
}
.modal-content.confirmation-dialog {
width: 400px;
max-width: 90%;
padding: 24px;
box-sizing: border-box;
opacity: 1 !important;
visibility: visible !important;
}
.history-empty .empty-icon {
width: 80px;
height: 80px;
margin-bottom: 16px;
color: var(--history-muted);
display: flex;
justify-content: center;
align-items: center;
}
.history-empty .empty-icon svg {
width: 100%;
height: 100%;
}
.globe-icon {
width: 16px !important;
height: 16px !important;
stroke: var(--history-muted);
display: block;
margin: 0 auto;
}
.history-item-favicon svg {
width: 16px;
height: 16px;
background: var(--history-hover);
border-radius: 4px;
padding: 2px;
}
.history-empty:not(.hidden) + .history-items {
display: none !important;
}
@media (min-width: 768px) {
.history-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.header-right {
justify-content: flex-end;
}
}
@media (max-width: 767px) {
.history-item-time {
display: none;
}
.history-item-actions {
opacity: 1;
}
.history-actions {
justify-content: space-between;
}
.history-actions button span {
display: none;
}
.history-actions button {
padding: 8px;
}
}

474
css/onboarding.css Normal file
View File

@ -0,0 +1,474 @@
.container-ob {
position: fixed;
inset: 0;
z-index: 10000;
background-color: var(--background);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.container-ob::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.step-ob {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
max-width: 800px;
width: 100%;
height: 100%;
padding: 2rem;
position: relative;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.step-ob.active-ob {
display: flex;
opacity: 1;
transform: translateY(0);
z-index: 1;
}
.title-ob {
font-size: 2.5rem;
font-weight: 700;
color: var(--text);
margin-bottom: 1rem;
transform-origin: center;
animation: titlePop-ob 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.subtitle-ob {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 2.5rem;
max-width: 600px;
}
.options-grid-ob {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
width: 100%;
margin-bottom: 2rem;
}
.step-ob[data-step="3"] .options-grid-ob {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
.step-ob[data-step="5"] .options-grid-ob {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
.option-card-ob {
background-color: var(--surface);
border: 2px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
overflow: hidden;
}
.option-card-ob:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px var(--shadow);
border-color: var(--text);
}
.option-card-ob.selected-ob {
border-color: var(--text);
background-color: var(--surface);
transform: scale(1.02);
}
.option-card-ob svg {
width: 48px;
height: 48px;
color: var(--text);
margin-bottom: 1rem;
}
.option-card-ob h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.5rem;
}
.option-card-ob p {
font-size: 0.875rem;
color: var(--text-secondary);
}
.option-card-ob[data-engine] img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1rem;
}
.nav-ob {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 800px;
padding: 0 1rem;
margin-top: auto;
margin-bottom: 40px;
position: relative;
z-index: 2;
}
.button-ob {
background-color: var(--surface);
border: 2px solid var(--border);
border-radius: 12px;
padding: 1rem 2rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.button-ob:hover {
background-color: var(--primary);
border-color: var(--text);
transform: translateY(-2px);
}
.button-ob svg {
width: 20px;
height: 20px;
}
.button-ob.primary-ob {
background-color: var(--text);
color: var(--background);
border-color: var(--text);
}
.button-ob.primary-ob:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow);
}
.button-ob.disabled-ob,
.button-ob:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.button-ob.disabled-ob:hover,
.button-ob:disabled:hover {
transform: none;
box-shadow: none;
background-color: var(--surface);
border-color: var(--border);
}
.progress-dots-ob {
display: flex;
gap: 8px;
}
.dot-ob {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--border);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dot-ob.active-ob {
background-color: var(--text);
transform: scale(1.2);
}
.theme-preview-ob {
width: 100% !important;
border-radius: 16px !important;
padding: 1.5rem !important;
margin-bottom: 1rem !important;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
border: 2px solid var(--border) !important;
}
.theme-preview-ob.light-ob {
background-color: #ffffff !important;
color: #1a1a1a !important;
border: 2px solid #eaeaea !important;
}
.theme-preview-ob.dark-ob {
background-color: #1a1a1a !important;
color: #ffffff !important;
border: 2px solid #333333 !important;
}
.theme-preview-ob.light-ob .preview-search-ob {
background-color: #f0f0f0 !important;
border-color: #dddddd !important;
}
.theme-preview-ob.dark-ob .preview-search-ob {
background-color: #333333 !important;
border-color: #444444 !important;
}
.option-card-ob[data-theme="light"] {
background-color: #ffffff !important;
border: 2px solid #eaeaea !important;
color: #1a1a1a !important;
}
.option-card-ob[data-theme="light"]:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08) !important;
border-color: #1a1a1a !important;
}
.option-card-ob[data-theme="light"].selected-ob {
border-color: #1a1a1a !important;
background-color: #ffffff !important;
transform: scale(1.02);
}
.option-card-ob[data-theme="light"] h3 {
color: #1a1a1a !important;
}
.option-card-ob[data-theme="light"] p {
color: #666666 !important;
}
.option-card-ob[data-theme="light"] svg {
color: #1a1a1a !important;
}
.option-card-ob[data-theme="dark"] {
background-color: #1a1a1a !important;
border: 2px solid #333333 !important;
color: #ffffff !important;
}
.option-card-ob[data-theme="dark"]:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2) !important;
border-color: #ffffff !important;
}
.option-card-ob[data-theme="dark"].selected-ob {
border-color: #ffffff !important;
background-color: #1a1a1a !important;
transform: scale(1.02);
}
.theme-preview-content-ob {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.preview-search-ob {
width: 100%;
height: 50px;
border-radius: 25px;
background-color: var(--surface);
border: 2px solid var(--border);
}
.preview-shortcuts-ob {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
width: 100%;
}
.preview-shortcut-ob {
width: 100%;
aspect-ratio: 1/1;
border-radius: 12px;
background-color: rgba(0, 0, 0, 0.1);
}
.dark-ob .preview-shortcut-ob {
background-color: rgba(255, 255, 255, 0.1);
}
.font-preview-ob {
font-size: 3rem;
margin-bottom: 1rem;
line-height: 1;
}
.name-input-container-ob {
width: 100%;
max-width: 500px;
margin: 2rem 0;
position: relative;
}
.name-input-ob {
width: 100%;
border: none !important;
background: transparent !important;
font-size: 4rem !important;
font-weight: 600 !important;
color: var(--text) !important;
text-align: left !important;
border-radius: 0 !important;
padding: 0.5rem 0 !important;
border-bottom: 2px solid #a3a3a3 !important;
transition: all 0.3s ease !important;
outline: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
appearance: none !important;
}
.name-input-ob:focus {
border-color: var(--text);
}
.name-input-ob::placeholder {
color: var(--text-secondary);
opacity: 0.5;
}
.name-input-ob:-webkit-autofill,
.name-input-ob:-webkit-autofill:hover,
.name-input-ob:-webkit-autofill:focus {
-webkit-text-fill-color: var(--text) !important;
-webkit-box-shadow: 0 0 0px 1000px transparent inset !important;
transition: background-color 5000s ease-in-out 0s !important;
background-clip: content-box !important;
}
@keyframes titlePop-ob {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes slideInUp-ob {
0% { transform: translateY(40px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
@keyframes fadeIn-ob {
0% { opacity: 0; }
100% { opacity: 1; }
}
.search-engine-preview-ob {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
}
.search-engine-preview-ob img {
width: 24px;
height: 24px;
border-radius: 50%;
}
.animate-in-ob {
animation: slideInUp-ob 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@media (max-width: 768px) {
.options-grid-ob {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.title-ob {
font-size: 2rem;
}
.subtitle-ob {
font-size: 1rem;
}
.name-input-ob {
font-size: 1.5rem;
}
.nav-ob {
padding: 0.5rem;
margin-bottom: 30px;
}
}
@media (max-width: 576px) {
.step-ob {
padding: 1.5rem 1rem;
}
.options-grid-ob,
.step-ob[data-step="3"] .options-grid-ob,
.step-ob[data-step="5"] .options-grid-ob {
grid-template-columns: 1fr;
gap: 1rem;
}
.option-card-ob {
padding: 1rem;
}
.title-ob {
font-size: 1.75rem;
}
.button-ob {
padding: 0.75rem 1.5rem;
}
.font-preview-ob {
font-size: 2.5rem;
}
}
@media (min-width: 577px) and (max-width: 991px) {
.step-ob[data-step="3"] .options-grid-ob,
.step-ob[data-step="5"] .options-grid-ob {
grid-template-columns: repeat(2, 1fr);
}
}

2718
css/style.css Normal file

File diff suppressed because it is too large Load Diff

144
history.html Normal file
View File

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Browsing History - JSTAR Tab</title>
<link rel="icon" href="/images/favicon.png">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/history.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Poppins:wght@400;500;600&family=Roboto:wght@400;500;700&family=Montserrat:wght@400;500;600&family=Quicksand:wght@400;500;600&display=swap" rel="stylesheet">
</head>
<body>
<!-- SVG Icons Definitions -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="icon-back" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 12H5M12 19l-7-7 7-7"></path>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</symbol>
<symbol id="icon-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</symbol>
<symbol id="icon-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</symbol>
<symbol id="icon-filter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</symbol>
<symbol id="icon-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</symbol>
<symbol id="icon-alert-triangle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</symbol>
</svg>
<div class="history-container">
<!-- Header -->
<header class="history-header">
<div class="header-left">
<button class="back-button" title="Back to Home">
<svg><use xlink:href="#icon-back"></use></svg>
</button>
<h1>Browsing History</h1>
</div>
<div class="header-right">
<div class="search-bar">
<svg><use xlink:href="#icon-search"></use></svg>
<input type="text" placeholder="Search history">
<button class="search-clear" title="Clear search">
<svg><use xlink:href="#icon-x"></use></svg>
</button>
</div>
</div>
</header>
<!-- Action Bar -->
<div class="history-actions">
<button id="delete-selected" disabled>
<svg><use xlink:href="#icon-trash"></use></svg>
<span>Delete Selected</span>
</button>
<button id="clear-all">
<svg><use xlink:href="#icon-trash"></use></svg>
<span>Clear All History</span>
</button>
</div>
<!-- Content Area -->
<div class="history-content">
<!-- Loading state -->
<div class="history-loading">
<div class="loading-spinner"></div>
<p>Loading your browsing history...</p>
</div>
<!-- Empty state -->
<div class="history-empty hidden">
<div class="empty-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="8" y1="15" x2="16" y2="15"></line>
<line x1="9" y1="9" x2="9.01" y2="9"></line>
<line x1="15" y1="9" x2="15.01" y2="9"></line>
</svg>
</div>
<h2>No history found</h2>
<p>There are no items in your browsing history that match your search.</p>
</div>
<!-- History items will be rendered here -->
<div class="history-items"></div>
<!-- Load more button -->
<div class="history-load-more hidden">
<button>Load More</button>
</div>
</div>
</div>
<!-- Confirmation Dialog -->
<div id="confirmation-dialog" class="modal hidden">
<div class="modal-backdrop"></div>
<div class="modal-content confirmation-dialog">
<div class="confirmation-icon">
<svg><use xlink:href="#icon-alert-triangle"/></svg>
</div>
<div class="confirmation-content">
<h3 id="confirmation-title">Delete items?</h3>
<p id="confirmation-message">Are you sure you want to delete these items from your history?</p>
</div>
<div class="modal-actions">
<button id="confirm-no" class="btn-primary">Cancel</button>
<button id="confirm-yes" class="btn-primary btn-danger">Delete</button>
</div>
</div>
</div>
<!-- Notification Container -->
<div id="notification-container"></div>
<script src="js/storage.js"></script>
<script src="js/notifications.js"></script>
<script src="js/history.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
images/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
images/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

BIN
images/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/secrecy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

1229
index.html

File diff suppressed because it is too large Load Diff

159
js/backgrounds.js Normal file
View File

@ -0,0 +1,159 @@
document.addEventListener('DOMContentLoaded', () => {
const MAX_IMAGE_SIZE_MB = 5;
const MAX_USER_BACKGROUNDS = 10;
const backgroundUpload = document.getElementById('background-upload');
const backgroundPreviewGrid = document.getElementById('background-preview-grid');
const resetBackground = document.getElementById('default-background');
resetBackground.style.order = '-1';
backgroundPreviewGrid.prepend(resetBackground);
loadBackgrounds();
setSavedBackground();
backgroundUpload.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const maxSizeBytes = MAX_IMAGE_SIZE_MB * 1024 * 1024;
if (file.size > maxSizeBytes) {
notifications.show(`Image exceeds size limit (max ${MAX_IMAGE_SIZE_MB} MB)`, 'error');
event.target.value = '';
return;
}
const existingBackgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
if (existingBackgrounds.length >= MAX_USER_BACKGROUNDS) {
notifications.show(`Maximum custom backgrounds limit reached!`, 'error');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function (e) {
const imageUrl = e.target.result;
addBackgroundPreview(imageUrl, false);
saveBackground(imageUrl);
setCustomBackground(imageUrl);
notifications.show('Background uploaded successfully!', 'success');
};
reader.readAsDataURL(file);
});
resetBackground.addEventListener('click', () => {
const currentBackground = Storage.get('customBackground');
if (!currentBackground) return;
document.body.style.backgroundImage = '';
Storage.remove('customBackground');
removeAllSelection();
notifications.show('Background reset to default!', 'success');
});
function loadBackgrounds() {
try {
const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
if (!Array.isArray(backgrounds)) {
throw new Error('Invalid backgrounds format');
}
backgrounds.forEach((bg) => {
if (typeof bg === 'string' &&
(bg.startsWith('data:image/') || bg.startsWith('images/backgrounds/'))) {
addBackgroundPreview(bg, false);
}
});
} catch (e) {
console.error('Failed to load backgrounds:', e);
Storage.set('backgrounds', '[]');
notifications.show('Corrupted backgrounds data - resetting', 'error');
}
}
function setSavedBackground() {
const customBackground = Storage.get('customBackground');
if (customBackground) {
setCustomBackground(customBackground, true);
}
}
function addBackgroundPreview(imageUrl, isPredefined) {
const preview = document.createElement('div');
preview.className = 'background-preview' + (isPredefined ? ' predefined' : ' custom');
preview.style.backgroundImage = `url(${imageUrl})`;
preview.dataset.url = imageUrl;
if (Storage.get('customBackground') === imageUrl) {
preview.classList.add('selected');
}
preview.addEventListener('click', () => {
if (Storage.get('customBackground') === imageUrl) return;
setCustomBackground(imageUrl);
removeAllSelection();
preview.classList.add('selected');
});
if (!isPredefined) {
const removeIcon = document.createElement('span');
removeIcon.className = 'remove-icon';
removeIcon.innerHTML = '<i class="fas fa-times"></i>';
removeIcon.addEventListener('click', (e) => {
e.stopPropagation();
const wasSelected = preview.classList.contains('selected');
if (wasSelected) {
document.body.style.backgroundImage = '';
Storage.remove('customBackground');
}
removeBackground(imageUrl);
preview.remove();
notifications.show('Background removed!', 'success');
});
preview.appendChild(removeIcon);
}
const insertPosition = isPredefined ? 1 : backgroundPreviewGrid.children.length;
backgroundPreviewGrid.insertBefore(preview, backgroundPreviewGrid.children[insertPosition]);
}
function removeAllSelection() {
document.querySelectorAll('.background-preview').forEach(preview => {
preview.classList.remove('selected');
});
}
function removeBackground(imageUrl) {
const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
Storage.set('backgrounds', JSON.stringify(backgrounds.filter(bg => bg !== imageUrl)));
}
function saveBackground(imageUrl) {
const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
if (!backgrounds.includes(imageUrl)) {
backgrounds.push(imageUrl);
Storage.set('backgrounds', JSON.stringify(backgrounds));
}
}
function setCustomBackground(imageUrl, initialLoad = false) {
document.body.style.backgroundImage = `url(${imageUrl})`;
Storage.set('customBackground', imageUrl);
if (!initialLoad) {
removeAllSelection();
const targetPreview = document.querySelector(`.background-preview[data-url="${imageUrl}"]`);
if (targetPreview) targetPreview.classList.add('selected');
}
}
const predefinedImages = [
'images/backgrounds/cherry.png',
'images/backgrounds/mommies.png',
'images/backgrounds/peachs-castle.png',
'images/backgrounds/windows-xp.jpg',
];
predefinedImages.forEach(image => addBackgroundPreview(image, true));
});

86
js/cache-handler.js Normal file
View File

@ -0,0 +1,86 @@
const CacheUpdater = {
update: () => {
const shortcuts = Storage.get('shortcuts') || [];
const faviconUrls = shortcuts.map(shortcut =>
`https://www.google.com/s2/favicons?domain=${encodeURIComponent(new URL(shortcut.url).hostname)}&sz=64`
);
if (navigator.serviceWorker?.controller && faviconUrls.length > 0) {
navigator.serviceWorker.controller.postMessage({
action: 'updateFavicons',
urls: faviconUrls
});
}
}
};
const cacheHandler = {
SHORTCUT_CACHE_DURATION: 7 * 24 * 60 * 60 * 1000,
init() {
this.cleanExpiredCache();
},
cleanExpiredCache() {
const cache = this.getAllCache();
const now = Date.now();
Object.entries(cache).forEach(([key, value]) => {
if (key.startsWith('shortcut_') && value.expiry && value.expiry < now) {
this.removeFromCache(key);
}
});
},
addToCache(key, value, isSearchEngine = false) {
const cacheItem = {
value,
timestamp: Date.now(),
expiry: isSearchEngine ? null : Date.now() + this.SHORTCUT_CACHE_DURATION
};
localStorage.setItem(key, JSON.stringify(cacheItem));
},
getFromCache(key) {
const item = localStorage.getItem(key);
if (!item) return null;
try {
const cacheItem = JSON.parse(item);
if (cacheItem.expiry && cacheItem.expiry < Date.now()) {
this.removeFromCache(key);
return null;
}
return cacheItem.value;
} catch (error) {
console.error('Cache read error:', error);
return null;
}
},
removeFromCache(key) {
localStorage.removeItem(key);
},
getAllCache() {
const cache = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('shortcut_') || key.startsWith('search_engine_')) {
try {
cache[key] = JSON.parse(localStorage.getItem(key));
} catch (error) {
console.error('Cache read error:', error);
}
}
}
return cache;
}
};
document.addEventListener('DOMContentLoaded', () => {
cacheHandler.init();
});

343
js/grid-layout.js Normal file
View File

@ -0,0 +1,343 @@
const GridLayout = {
defaults: {
type: 'default',
columns: 6,
gap: 16,
size: 80,
resizable: false
},
layouts: {
default: {
columns: 6,
gap: 16,
size: 80
},
compact: {
columns: 5,
gap: 8,
size: 70
},
comfortable: {
columns: 3,
gap: 24,
size: 100
},
list: {
columns: 1,
gap: 12,
size: 60
},
custom: {}
},
init: function() {
const settings = this.getSettings();
this.applyLayout(settings);
this.setupEventListeners();
this.toggleCustomSettings(settings.type === 'custom');
this.updateUI(settings);
},
getSettings: function() {
const storedSettings = Storage.get('gridLayout');
return { ...this.defaults, ...storedSettings };
},
saveSettings: function(settings) {
Storage.set('gridLayout', settings);
},
updateUI: function(settings) {
const layoutTypeSelect = document.getElementById('grid-layout-type');
if (layoutTypeSelect) {
layoutTypeSelect.value = settings.type;
const event = new Event('change');
layoutTypeSelect.dispatchEvent(event);
const customSelectDiv = layoutTypeSelect.closest('.custom-select');
if (customSelectDiv) {
const selectSelected = customSelectDiv.querySelector('.select-selected');
if (selectSelected) {
const selectedOption = layoutTypeSelect.options[layoutTypeSelect.selectedIndex];
selectSelected.textContent = selectedOption.textContent;
}
}
}
const columnsInput = document.getElementById('grid-columns');
const gapInput = document.getElementById('grid-gap');
const sizeInput = document.getElementById('grid-size');
if (columnsInput) columnsInput.value = settings.columns;
if (gapInput) gapInput.value = settings.gap;
if (sizeInput) {
sizeInput.value = settings.size;
sizeInput.disabled = !settings.resizable;
if (!settings.resizable) {
sizeInput.setAttribute('title', 'Enable \'Resizable Items\' to customize item size');
sizeInput.style.cursor = 'not-allowed';
} else {
sizeInput.removeAttribute('title');
sizeInput.style.cursor = 'auto';
}
}
const resizableToggle = document.getElementById('toggle-resizable');
if (resizableToggle) resizableToggle.checked = settings.resizable;
this.toggleCustomSettings(settings.type === 'custom');
},
applyLayout: function(settings) {
const grid = document.getElementById('shortcuts-grid');
if (!grid) return;
grid.classList.remove('grid-default', 'grid-compact', 'grid-comfortable', 'grid-list', 'grid-custom');
grid.classList.add(`grid-${settings.type}`);
if (settings.type === 'custom') {
grid.style.gridTemplateColumns = `repeat(${settings.columns}, minmax(${settings.size}px, 1fr))`;
grid.style.gap = `${settings.gap}px`;
} else {
const layoutConfig = this.layouts[settings.type];
if (layoutConfig) {
grid.style.gridTemplateColumns = `repeat(${layoutConfig.columns}, minmax(${layoutConfig.size}px, 1fr))`;
grid.style.gap = `${layoutConfig.gap}px`;
if (settings.type === 'list') {
grid.style.gridTemplateColumns = '1fr';
}
} else {
grid.style.gridTemplateColumns = '';
grid.style.gap = '';
}
}
this.applyResizable(settings.resizable);
},
setupEventListeners: function() {
const layoutTypeSelect = document.getElementById('grid-layout-type');
if (layoutTypeSelect) {
layoutTypeSelect.addEventListener('change', () => {
const settings = this.getSettings();
settings.type = layoutTypeSelect.value;
this.saveSettings(settings);
this.applyLayout(settings);
this.toggleCustomSettings(settings.type === 'custom');
});
}
const columnsInput = document.getElementById('grid-columns');
const gapInput = document.getElementById('grid-gap');
const sizeInput = document.getElementById('grid-size');
const validateInputValue = (input) => {
const min = parseInt(input.getAttribute('min'), 10);
const max = parseInt(input.getAttribute('max'), 10);
let value = parseInt(input.value, 10);
if (isNaN(value)) {
value = parseInt(input.defaultValue, 10);
}
if (value < min) value = min;
if (value > max) value = max;
input.value = value;
return value;
};
[columnsInput, gapInput, sizeInput].forEach(input => {
if (input) {
input.addEventListener('input', () => {
validateInputValue(input);
});
input.addEventListener('change', () => {
const value = validateInputValue(input);
const settings = this.getSettings();
settings[input.id.split('-')[1]] = value;
this.saveSettings(settings);
this.applyLayout(settings);
});
}
});
const resizableToggle = document.getElementById('toggle-resizable');
if (resizableToggle) {
resizableToggle.addEventListener('change', () => {
const settings = this.getSettings();
settings.resizable = resizableToggle.checked;
this.saveSettings(settings);
this.applyLayout(settings);
this.toggleItemSizeInput(settings.resizable);
});
}
const resetButton = document.getElementById('reset-layout');
if (resetButton) {
resetButton.addEventListener('click', () => {
shortcuts.showConfirmDialog(
'Reset Layout',
'Are you sure you want to reset the layout settings to default?',
() => this.resetToDefaults()
);
});
}
},
toggleItemSizeInput: function(enabled) {
const sizeInput = document.getElementById('grid-size');
if (sizeInput) {
sizeInput.disabled = !enabled;
if (enabled) {
sizeInput.removeAttribute('title');
sizeInput.style.cursor = 'auto';
} else {
sizeInput.setAttribute('title', 'Enable \'Resizable Items\' to customize item size');
sizeInput.style.cursor = 'not-allowed';
}
}
},
toggleCustomSettings: function(show) {
const customSettings = document.getElementById('custom-grid-settings');
if (customSettings) {
if (show) {
customSettings.classList.remove('hidden');
} else {
customSettings.classList.add('hidden');
}
}
},
applyResizable: function(resizable) {
const shortcuts = document.querySelectorAll('.shortcut');
shortcuts.forEach(shortcut => {
const existingHandle = shortcut.querySelector('.resize-handle');
if (existingHandle) {
existingHandle.remove();
}
shortcut.classList.remove('resizable');
if (resizable) {
shortcut.classList.add('resizable');
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
shortcut.appendChild(resizeHandle);
this.setupResizeEvents(shortcut, resizeHandle);
}
});
},
setupResizeEvents: function(shortcut, handle) {
let startX, startY, startWidth, startHeight;
const startResize = (e) => {
e.preventDefault();
shortcut.classList.add('resizing');
startX = e.clientX;
startY = e.clientY;
startWidth = shortcut.offsetWidth;
startHeight = shortcut.offsetHeight;
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
};
const resize = (e) => {
const newWidth = startWidth + (e.clientX - startX);
const newHeight = startHeight + (e.clientY - startY);
shortcut.style.width = `${Math.max(80, newWidth)}px`;
shortcut.style.height = `${Math.max(80, newHeight)}px`;
};
const stopResize = () => {
shortcut.classList.remove('resizing');
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
};
handle.addEventListener('mousedown', startResize);
},
resetToDefaults: function() {
this.saveSettings(this.defaults);
this.updateUI(this.defaults);
this.applyLayout(this.defaults);
this.toggleCustomSettings(false);
const visibilitySettings = {
showGreeting: true,
showSearch: true,
showShortcuts: true,
showAddButton: true,
showGrid: true
};
Storage.set('visibility', visibilitySettings);
Storage.set('show_greeting', true);
Storage.set('show_search', true);
Storage.set('show_shortcuts', true);
Storage.set('show_addShortcut', true);
const greetingToggle = document.getElementById('toggle-greeting');
const searchToggle = document.getElementById('toggle-search');
const shortcutsToggle = document.getElementById('toggle-shortcuts');
const addButtonToggle = document.getElementById('toggle-add-shortcut');
if (greetingToggle) greetingToggle.checked = true;
if (searchToggle) searchToggle.checked = true;
if (shortcutsToggle) shortcutsToggle.checked = true;
if (addButtonToggle) addButtonToggle.checked = true;
const greeting = document.getElementById('greeting');
const search = document.getElementById('search-container');
const shortcuts = document.getElementById('shortcuts-grid');
const addButton = document.getElementById('add-shortcut');
const showElement = (element) => {
if (element) {
element.style.visibility = 'visible';
element.style.opacity = '1';
element.style.position = 'relative';
element.style.pointerEvents = 'auto';
}
};
showElement(greeting);
showElement(search);
showElement(shortcuts);
showElement(addButton);
},
reset: function() {
this.resetToDefaults();
notifications.show('Layout settings reset to defaults.', 'success');
return this.defaults;
}
};
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();
}
}, 500);
});

904
js/history.js Normal file
View File

@ -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 = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="globe-icon">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
`;
};
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 = '<svg><use xlink:href="#icon-external-link"></use></svg>';
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 = '<svg><use xlink:href="#icon-trash"></use></svg>';
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);
};
}
});

313
js/keybinds.js Normal file
View File

@ -0,0 +1,313 @@
const FORBIDDEN_KEYS = [
'Tab', 'CapsLock', 'Meta', 'ContextMenu',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', 'ScrollLock', 'Pause', 'NumLock',
'/'
];
const keybinds = {
bindings: {},
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;
function isValidUrl(string) {
try {
const urlString = string.match(/^https?:\/\//) ? string : `https://${string}`;
new URL(urlString);
return true;
} catch (_) {
return false;
}
}
urlInput.addEventListener('input', () => {
if (!this.bindings.url) {
this.bindings.url = { url: '', keys: '' };
}
this.bindings.url.url = urlInput.value;
Storage.set('keybinds', this.bindings);
});
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
urlInput.blur();
}
});
urlInput.addEventListener('blur', () => {
const currentUrl = urlInput.value.trim();
if (currentUrl === lastSavedUrl) {
return;
}
if (currentUrl) {
if (isValidUrl(currentUrl)) {
lastSavedUrl = currentUrl;
notifications.show('URL saved.', 'success');
} else {
notifications.show('Please enter a valid URL.', 'error');
urlInput.value = lastSavedUrl;
this.bindings.url.url = lastSavedUrl;
Storage.set('keybinds', this.bindings);
}
}
});
const keybindInputs = document.querySelectorAll('[id^="keybind-"]');
keybindInputs.forEach(input => {
if (input.id === 'keybind-url') {
const urlBinding = this.bindings['url'];
if (urlBinding && urlBinding.url) {
input.value = urlBinding.url;
}
input.addEventListener('input', () => {
if (this.bindings['url']) {
this.bindings['url'].url = input.value;
Storage.set('keybinds', this.bindings);
}
});
return;
}
const action = input.id.replace('keybind-url-combo', 'url').replace('keybind-', '');
if (this.bindings[action]) {
input.value = this.bindings[action].keys;
}
let currentKeys = new Set();
let isProcessingKeybind = false;
input.addEventListener('keydown', (e) => {
e.preventDefault();
if (e.key === 'Escape') {
input.blur();
return;
}
if (e.ctrlKey) {
notifications.show('CTRL key combinations are not allowed.', 'error');
isProcessingKeybind = true;
return;
}
if (FORBIDDEN_KEYS.includes(e.key)) {
notifications.show('This key cannot be used as a keybind.', 'error');
isProcessingKeybind = true;
return;
}
isProcessingKeybind = false;
if (e.key !== 'Alt' && e.key !== 'Shift') {
currentKeys.add(e.key);
}
if (e.altKey) currentKeys.add('Alt');
if (e.shiftKey) currentKeys.add('Shift');
input.value = Array.from(currentKeys).join('+');
});
input.addEventListener('keyup', (e) => {
if (isProcessingKeybind) {
currentKeys.clear();
return;
}
if (e.key === 'Alt' || e.key === 'Shift') {
if (currentKeys.size === 1) {
notifications.show('Add another key with Alt or Shift.', 'error');
}
currentKeys.clear();
input.value = this.bindings[action]?.keys || '';
return;
}
const combo = Array.from(currentKeys).join('+');
if (!combo) return;
const duplicate = Object.entries(this.bindings).find(([key, value]) =>
value.keys === combo && key !== action
);
if (duplicate) {
notifications.show('This keybind is already in use.', 'error');
currentKeys.clear();
input.value = this.bindings[action]?.keys || '';
return;
}
this.bindings[action] = {
keys: combo,
url: action === 'url' ? document.getElementById('keybind-url').value : null
};
Storage.set('keybinds', this.bindings);
notifications.show('Keybind saved.', 'success');
});
input.addEventListener('blur', () => {
currentKeys.clear();
input.value = this.bindings[action]?.keys || '';
});
});
document.querySelectorAll('.clear-keybind').forEach(button => {
button.addEventListener('click', () => {
const action = button.dataset.for;
const input = document.getElementById(`keybind-${action}-combo`) ||
document.getElementById(`keybind-${action}`);
input.value = '';
if (action === 'url') {
document.getElementById('keybind-url').value = '';
}
delete this.bindings[action];
Storage.set('keybinds', this.bindings);
notifications.show('Keybind removed.', 'success');
});
});
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || !Storage.get('onboardingComplete')) return;
const keys = [];
if (e.altKey) keys.push('Alt');
if (e.shiftKey) keys.push('Shift');
if (e.key !== 'Alt' && e.key !== 'Shift') keys.push(e.key);
const combo = keys.join('+');
Object.entries(this.bindings).forEach(([action, binding]) => {
if (binding.keys === combo) {
e.preventDefault();
this.executeAction(action, binding);
}
});
});
},
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');
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');
}, 300);
}
const activeModal = document.querySelector('.modal.active');
switch (action) {
case 'settings':
if (settingsPage.classList.contains('hidden')) {
settings.updateSettingsUI();
settingsPage.classList.remove('hidden');
setTimeout(() => {
settingsPage.classList.add('active');
}, 10);
} else {
settings.updateSettingsUI();
}
break;
case 'add-shortcut':
const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
return;
}
const shortcutModal = document.getElementById('add-shortcut-modal');
if (shortcutModal === activeModal) {
closeModal(shortcutModal);
} else {
openModal(shortcutModal);
}
break;
case 'anonymous':
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}`;
window.location.href = fullUrl;
}
break;
}
}
};

145
js/main.js Normal file
View File

@ -0,0 +1,145 @@
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
async function updateGreeting() {
const greeting = document.getElementById('greeting');
if (!greeting) return;
const customFormat = Storage.get('customGreeting');
if (customFormat) {
const formattedGreeting = await settings.formatGreeting(customFormat);
if (formattedGreeting) {
greeting.textContent = formattedGreeting;
greeting.style.opacity = '0';
setTimeout(() => {
greeting.style.opacity = '1';
}, 100);
return;
}
}
const hour = new Date().getHours();
const isAnonymous = Storage.get('anonymousMode') || false;
const userName = isAnonymous ?
(Storage.get('anonymousName') || anonymousNames.generate()) :
(Storage.get('userName') || 'Friend');
let timeGreeting = 'Hello';
if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning';
else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon';
else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening';
else timeGreeting = 'Good Night';
greeting.textContent = `${timeGreeting}, ${userName}!`;
greeting.style.opacity = '0';
setTimeout(() => {
greeting.style.opacity = '1';
}, 100);
}
function initModalHandlers() {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal && !modal.classList.contains('onboarding-modal')) {
closeModal(modal);
}
});
const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
modalContent.addEventListener('click', (e) => {
e.stopPropagation();
});
}
document.querySelectorAll('.modal .close-button').forEach(button => {
button.addEventListener('click', () => {
const modal = button.closest('.modal');
if (modal) {
closeModal(modal);
}
});
});
});
}
function openModal(modal) {
if (!modal) return;
const contextMenu = document.querySelector('.context-menu');
if (contextMenu) {
contextMenu.classList.add('hidden');
}
modal.classList.remove('hidden');
requestAnimationFrame(() => {
modal.classList.add('active');
});
}
function closeModal(modal) {
if (!modal) return;
modal.classList.remove('active');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
document.addEventListener('DOMContentLoaded', () => {
if (typeof settings !== 'undefined' && typeof settings.updateVisibility === 'function') {
settings.updateVisibility();
} else {
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
const isVisible = Storage.get(`show_${element}`);
if (isVisible === false) {
const elementNode = document.getElementById(element === 'search' ? 'search-container' :
element === 'addShortcut' ? 'add-shortcut' : element);
if (elementNode) {
elementNode.style.visibility = 'hidden';
elementNode.style.opacity = '0';
elementNode.style.position = 'absolute';
elementNode.style.pointerEvents = 'none';
}
}
});
}
if (!Storage.get('onboardingComplete')) {
onboarding.start();
} else {
document.getElementById('main-content').classList.remove('hidden');
}
search.init();
shortcuts.init();
settings.init();
initModalHandlers();
updateGreeting();
setInterval(updateGreeting, 60000);
const settingsButton = document.getElementById('settings-button');
const settingsModal = document.getElementById('settings-modal');
settingsButton.addEventListener('click', () => {
openModal(settingsModal);
});
keybinds.init();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const activeModal = document.querySelector('.modal.active');
if (activeModal && !activeModal.matches('#settings-modal')) {
const primaryButton = activeModal.querySelector('.btn-primary');
if (primaryButton) {
primaryButton.click();
}
}
}
});

121
js/notifications.js Normal file
View File

@ -0,0 +1,121 @@
class NotificationSystem {
constructor() {
this.container = document.getElementById('notification-container');
this.notifications = new Map();
}
show(message, type = 'info', duration = 3000) {
const id = Date.now().toString();
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
const icon = this.createIcon(type);
const content = this.createContent(message);
const closeBtn = this.createCloseButton(id);
const progress = this.createProgressBar(type);
notification.appendChild(icon);
notification.appendChild(content);
notification.appendChild(closeBtn);
notification.appendChild(progress);
this.container.appendChild(notification);
setTimeout(() => this.remove(id), duration);
this.notifications.set(id, {
element: notification,
duration
});
this.updateProgress(id);
return id;
}
remove(id) {
const notification = this.notifications.get(id);
if (notification) {
notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)';
setTimeout(() => {
notification.element.remove();
this.notifications.delete(id);
}, 300);
}
}
updateProgress(id) {
const notification = this.notifications.get(id);
if (notification) {
const progress = notification.element.querySelector('.notification-progress');
const startTime = Date.now();
const update = () => {
const elapsed = Date.now() - startTime;
const percent = 100 - (elapsed / notification.duration * 100);
if (percent > 0) {
progress.style.width = `${percent}%`;
requestAnimationFrame(update);
}
};
requestAnimationFrame(update);
}
}
createIcon(type) {
const icon = document.createElement('i');
switch(type) {
case 'success':
icon.className = 'fas fa-check-circle';
icon.style.color = 'var(--success-color, #4caf50)';
break;
case 'error':
icon.className = 'fas fa-times-circle';
icon.style.color = 'var(--error-color, #f44336)';
break;
case 'info':
default:
icon.className = 'fas fa-info-circle';
icon.style.color = 'var(--info-color, #2196f3)';
break;
}
return icon;
}
createContent(message) {
const content = document.createElement('div');
content.className = 'notification-content';
content.innerHTML = message;
return content;
}
createCloseButton(id) {
const closeBtn = document.createElement('button');
closeBtn.className = 'notification-close';
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
closeBtn.onclick = () => this.remove(id);
return closeBtn;
}
createProgressBar(type) {
const progress = document.createElement('div');
progress.className = 'notification-progress';
switch(type) {
case 'success':
progress.style.background = 'var(--success-color, #4caf50)';
break;
case 'error':
progress.style.background = 'var(--error-color, #f44336)';
break;
case 'info':
default:
progress.style.background = 'var(--info-color, #2196f3)';
break;
}
return progress;
}
}
const notifications = new NotificationSystem();

337
js/onboarding.js Normal file
View File

@ -0,0 +1,337 @@
const onboarding = {
currentStep: 1,
totalSteps: 5,
settings: {},
lastNotification: 0,
isCompleting: false,
notificationShown: false,
showNotification(message, type = 'info') {
const now = Date.now();
if (now - this.lastNotification >= 500) {
this.lastNotification = now;
notifications.show(message, type);
}
},
isComplete: () => {
return Storage.get('onboardingComplete') === true;
},
start: () => {
const onboardingContainer = document.getElementById('onboarding-container');
const mainContent = document.getElementById('main-content');
const fileInput = document.getElementById('onboarding-import');
document.getElementById('notification-container').style.zIndex = "20000";
if (!onboarding.isComplete()) {
document.body.style.overflow = 'hidden';
onboardingContainer.classList.remove('hidden');
onboarding.initProgressDots();
onboarding.setupEventListeners();
const theme = Storage.get('theme') || 'light';
document.body.setAttribute('data-theme', theme);
document.querySelectorAll('.step-ob').forEach(step => {
if (step.dataset.step !== "1") {
step.classList.remove('active-ob');
}
});
const firstStep = document.querySelector('.step-ob[data-step="1"]');
firstStep.classList.add('active-ob');
document.getElementById('prev-step').style.visibility = 'hidden';
const nextButton = document.getElementById('next-step');
nextButton.innerHTML = 'Next <svg><use href="#icon-arrow-right"/></svg>';
if (onboarding.currentStep > 1) {
nextButton.disabled = true;
nextButton.classList.add('disabled-ob');
}
mainContent.classList.add('hidden');
} else {
mainContent.classList.remove('hidden');
}
fileInput.addEventListener('change', async (e) => {
if (e.target.files.length > 0) {
try {
const file = e.target.files[0];
const text = await file.text();
const data = JSON.parse(text);
if (!data.settings || !data.shortcuts || !Array.isArray(data.shortcuts)) {
throw new Error('Invalid data structure');
}
Object.entries(data.settings).forEach(([key, value]) => {
Storage.set(key, value);
});
Storage.set('shortcuts', data.shortcuts);
if (data.keybinds) {
Storage.set('keybinds', data.keybinds);
}
Storage.set('onboardingComplete', true);
localStorage.setItem('showWelcomeAfterImport', 'true');
window.location.reload();
} catch (error) {
onboarding.showNotification('Failed to import data: Invalid file format!', 'error');
fileInput.value = '';
}
}
});
},
setupEventListeners: () => {
document.querySelectorAll('.option-card-ob').forEach(card => {
card.addEventListener('click', () => {
const step = card.closest('.step-ob');
const stepNumber = parseInt(step.dataset.step);
const cards = step.querySelectorAll('.option-card-ob');
const nextButton = document.getElementById('next-step');
if (card.dataset.action === 'import-data') {
if (!card.classList.contains('selected-ob')) {
document.getElementById('onboarding-import').click();
}
cards.forEach(c => c.classList.remove('selected-ob'));
card.classList.add('selected-ob');
return;
}
cards.forEach(c => c.classList.remove('selected-ob'));
card.classList.add('selected-ob');
card.style.transform = 'scale(1.05)';
setTimeout(() => {
card.style.transform = 'scale(1.02)';
}, 150);
nextButton.disabled = false;
nextButton.classList.remove('disabled-ob');
if (card.dataset.theme) {
onboarding.settings.theme = card.dataset.theme;
document.body.setAttribute('data-theme', card.dataset.theme);
} else if (card.dataset.font) {
onboarding.settings.fontFamily = card.dataset.font;
document.documentElement.style.setProperty('--font-family', card.dataset.font);
} else if (card.dataset.engine) {
onboarding.settings.searchEngine = card.dataset.engine;
}
});
});
const nameInput = document.getElementById('user-name');
const nextButton = document.getElementById('next-step');
nameInput.addEventListener('input', (e) => {
const name = e.target.value.trim();
if (name) {
onboarding.settings.userName = name;
if (onboarding.currentStep === 4) {
nextButton.disabled = false;
nextButton.classList.remove('disabled-ob');
}
} else {
if (onboarding.currentStep === 4) {
nextButton.disabled = true;
nextButton.classList.add('disabled-ob');
}
}
});
document.getElementById('prev-step').addEventListener('click', () => {
if (onboarding.currentStep > 1) {
onboarding.navigateToStep(onboarding.currentStep - 1);
}
});
document.getElementById('next-step').addEventListener('click', () => {
let canProceed = true;
if (onboarding.currentStep === 2 && !onboarding.settings.theme) {
onboarding.showNotification('Please select a theme!', 'error');
canProceed = false;
}
else if (onboarding.currentStep === 3 && !onboarding.settings.fontFamily) {
onboarding.showNotification('Please select a font!', 'error');
canProceed = false;
}
else if (onboarding.currentStep === 4) {
const name = document.getElementById('user-name').value.trim();
if (!name) {
onboarding.showNotification('Please enter your name!', 'error');
canProceed = false;
} else {
onboarding.settings.userName = name;
}
}
else if (onboarding.currentStep === 5 && !onboarding.settings.searchEngine) {
onboarding.showNotification('Please select a search engine!', 'error');
canProceed = false;
}
if (canProceed) {
if (onboarding.currentStep < onboarding.totalSteps) {
onboarding.navigateToStep(onboarding.currentStep + 1);
} else {
onboarding.isCompleting = true;
localStorage.setItem('showWelcomeAfterImport', 'true');
onboarding.complete();
}
}
});
document.querySelectorAll('.step-ob').forEach(step => {
if (step.dataset.step !== "1" && step.dataset.step !== "4") {
const firstOption = step.querySelector('.option-card-ob');
if (firstOption) {
setTimeout(() => {
firstOption.click();
}, 100);
}
}
});
},
navigateToStep: (step) => {
const prevButton = document.getElementById('prev-step');
const nextButton = document.getElementById('next-step');
const currentStepEl = document.querySelector(`.step-ob[data-step="${onboarding.currentStep}"]`);
if (currentStepEl) {
currentStepEl.classList.remove('active-ob');
}
setTimeout(() => {
const targetStepEl = document.querySelector(`.step-ob[data-step="${step}"]`);
if (targetStepEl) {
targetStepEl.classList.add('active-ob');
}
onboarding.currentStep = step;
prevButton.style.visibility = step === 1 ? 'hidden' : 'visible';
if (step === onboarding.totalSteps) {
nextButton.innerHTML = 'Get Started <svg><use href="#icon-sparkle"/></svg>';
} else {
nextButton.innerHTML = 'Next <svg><use href="#icon-arrow-right"/></svg>';
}
if ((step === 2 && !onboarding.settings.theme) ||
(step === 3 && !onboarding.settings.fontFamily) ||
(step === 5 && !onboarding.settings.searchEngine)) {
nextButton.disabled = true;
nextButton.classList.add('disabled-ob');
} else if (step === 4) {
const name = document.getElementById('user-name').value.trim();
if (!name) {
nextButton.disabled = true;
nextButton.classList.add('disabled-ob');
} else {
nextButton.disabled = false;
nextButton.classList.remove('disabled-ob');
}
} else if (step === 1) {
nextButton.disabled = false;
nextButton.classList.remove('disabled-ob');
}
onboarding.updateProgressDots();
}, 100);
},
initProgressDots: () => {
const container = document.querySelector('.progress-dots-ob');
container.innerHTML = '';
for (let i = 0; i < onboarding.totalSteps; i++) {
const dot = document.createElement('div');
dot.className = 'dot-ob' + (i === 0 ? ' active-ob' : '');
container.appendChild(dot);
}
},
updateProgressDots: () => {
const dots = document.querySelectorAll('.dot-ob');
dots.forEach((dot, index) => {
dot.classList.toggle('active-ob', index + 1 === onboarding.currentStep);
});
},
complete: () => {
const onboardingContainer = document.getElementById('onboarding-container');
const mainContent = document.getElementById('main-content');
if (!onboarding.settings.theme) onboarding.settings.theme = 'light';
if (!onboarding.settings.fontFamily) onboarding.settings.fontFamily = 'Inter';
if (!onboarding.settings.searchEngine) onboarding.settings.searchEngine = 'google';
if (!onboarding.settings.userName) onboarding.settings.userName = 'User';
document.body.setAttribute('data-theme', onboarding.settings.theme);
document.documentElement.style.setProperty('--font-family', onboarding.settings.fontFamily);
Object.entries(onboarding.settings).forEach(([key, value]) => {
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(() => {
onboardingContainer.classList.add('hidden');
mainContent.classList.remove('hidden');
document.body.style.overflow = '';
search.init();
shortcuts.init();
settings.init();
updateGreeting();
setTimeout(() => {
if (!onboarding.notificationShown) {
onboarding.notificationShown = true;
onboarding.showNotification('Welcome to your new JSTAR Tab! 🎉', 'success');
}
}, 100);
}, 500);
}
};
document.addEventListener('DOMContentLoaded', () => {
onboarding.start();
if (onboarding.isComplete() && localStorage.getItem('showWelcomeAfterImport') === 'true') {
localStorage.removeItem('showWelcomeAfterImport');
setTimeout(() => {
notifications.show('Welcome to your new JSTAR Tab! 🎉', 'success');
}, 500);
}
});

80
js/search.js Normal file
View File

@ -0,0 +1,80 @@
const search = {
engines: {
google: {
url: 'https://www.google.com/search?q=',
icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=32',
name: 'Google'
},
bing: {
url: 'https://www.bing.com/search?q=',
icon: 'https://www.google.com/s2/favicons?domain=bing.com&sz=32',
name: 'Bing'
},
duckduckgo: {
url: 'https://duckduckgo.com/?q=',
icon: 'https://www.google.com/s2/favicons?domain=duckduckgo.com&sz=32',
name: 'DuckDuckGo'
},
brave: {
url: 'https://search.brave.com/search?q=',
icon: 'https://www.google.com/s2/favicons?domain=brave.com&sz=32',
name: 'Brave'
},
qwant: {
url: 'https://www.qwant.com/?q=',
icon: 'https://www.google.com/s2/favicons?domain=qwant.com&sz=32',
name: 'Qwant'
},
searxng: {
url: 'https://searx.be/search?q=',
icon: 'https://www.google.com/s2/favicons?domain=searx.be&sz=32',
name: 'SearXNG'
}
},
init: () => {
const searchBar = document.getElementById('search-bar');
const searchButton = document.getElementById('search-button');
searchBar.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
search.perform();
}
});
searchButton.addEventListener('click', search.perform);
document.addEventListener('keydown', (e) => {
if (e.key === '/' &&
!['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) &&
window.getSelection().toString() === '') {
e.preventDefault();
searchBar.focus();
}
});
const searchEngine = Storage.get('searchEngine') || 'google';
search.updateSearchEngineIcon(searchEngine);
},
updateSearchEngineIcon(engine) {
const searchIcon = document.querySelector('#search-container .search-icon img');
if (!searchIcon) return;
searchIcon.src = this.engines[engine].icon;
},
perform: () => {
const searchBar = document.getElementById('search-bar');
const query = searchBar.value.trim();
const engine = Storage.get('searchEngine') || 'google';
if (query) {
const searchUrl = search.engines[engine].url + encodeURIComponent(query);
window.location.href = searchUrl;
}
}
};
document.addEventListener('DOMContentLoaded', () => {
search.init();
});

1164
js/settings.js Normal file

File diff suppressed because it is too large Load Diff

825
js/shortcuts.js Normal file
View File

@ -0,0 +1,825 @@
const shortcuts = {
MAX_SHORTCUTS: 12,
validateAndFormatUrl: (url) => {
if (!/^https?:\/\//i.test(url)) {
url = 'https://' + url;
}
try {
new URL(url);
return url;
} catch (e) {
return false;
}
},
add: (url, name, isPasswordProtected = false) => {
const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
notifications.show('Maximum shortcuts limit reached!', 'error');
return;
}
const formattedUrl = shortcuts.validateAndFormatUrl(url);
if (!formattedUrl) {
notifications.show('Invalid URL format!', 'error');
return;
}
currentShortcuts.push({
url: formattedUrl,
name,
isPasswordProtected: isPasswordProtected || false
});
Storage.set('shortcuts', currentShortcuts);
shortcuts.render();
CacheUpdater.update();
},
remove: (index) => {
const currentShortcuts = Storage.get('shortcuts') || [];
currentShortcuts.splice(index, 1);
Storage.set('shortcuts', currentShortcuts);
shortcuts.render();
notifications.show('Shortcut removed!', 'success');
CacheUpdater.update();
},
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,
isPasswordProtected: isPasswordProtected || false
};
Storage.set('shortcuts', currentShortcuts);
shortcuts.render();
notifications.show('Shortcut updated!', 'success');
CacheUpdater.update();
},
showContextMenu: (e, index) => {
e.preventDefault();
const menu = document.getElementById('context-menu');
const rect = e.target.getBoundingClientRect();
menu.style.top = `${e.clientY}px`;
menu.style.left = `${e.clientX}px`;
menu.classList.remove('hidden');
menu.dataset.shortcutIndex = index;
const handleClickOutside = (event) => {
if (!menu.contains(event.target)) {
menu.classList.add('hidden');
document.removeEventListener('click', handleClickOutside);
}
};
setTimeout(() => {
document.addEventListener('click', handleClickOutside);
}, 0);
},
render: () => {
const grid = document.getElementById('shortcuts-grid');
const currentShortcuts = Storage.get('shortcuts') || [];
const isAnonymous = Storage.get('anonymousMode') || false;
grid.innerHTML = '';
currentShortcuts.forEach((shortcut, index) => {
const element = document.createElement('div');
element.className = `shortcut ${isAnonymous ? 'blurred' : ''} ${shortcut.isPasswordProtected ? 'password-protected' : ''}`;
element.dataset.index = index;
const icon = document.createElement('img');
icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
icon.alt = shortcut.name;
icon.draggable = false;
const name = document.createElement('span');
name.textContent = shortcut.name;
element.appendChild(icon);
element.appendChild(name);
element.addEventListener('click', (e) => {
if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) {
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');
}
}
});
element.addEventListener('contextmenu', (e) => {
e.preventDefault();
const menu = document.getElementById('context-menu');
menu.style.top = `${e.pageY}px`;
menu.style.left = `${e.pageX}px`;
menu.classList.remove('hidden');
menu.dataset.shortcutIndex = index;
const closeMenu = (event) => {
if (!menu.contains(event.target)) {
menu.classList.add('hidden');
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
});
grid.appendChild(element);
});
},
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');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
modal.classList.remove('active');
setTimeout(() => {
modal.classList.add('hidden');
document.getElementById('shortcut-url').value = '';
document.getElementById('shortcut-name').value = '';
}, 300);
});
}
if (addShortcutButton) {
addShortcutButton.addEventListener('click', (e) => {
e.stopPropagation();
const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
notifications.show('Maximum shortcuts limit reached!', 'error');
return;
}
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('active');
const urlInput = document.getElementById('shortcut-url');
const nameInput = document.getElementById('shortcut-name');
const saveShortcutButton = document.getElementById('save-shortcut');
if (saveShortcutButton) {
saveShortcutButton.onclick = () => {
const url = urlInput.value.trim();
const name = nameInput.value.trim();
if (url && name) {
try {
new URL(url);
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) {
notifications.show('Invalid URL format!', 'error');
}
}
};
}
const cancelShortcutButton = document.getElementById('cancel-shortcut');
if (cancelShortcutButton) {
cancelShortcutButton.onclick = () => {
modal.classList.remove('active');
setTimeout(() => {
modal.classList.add('hidden');
urlInput.value = '';
nameInput.value = '';
const passwordProtectCheckbox = document.getElementById('protect-shortcut');
if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false;
}, 300);
};
}
}
});
}
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');
const masterPasswordInput = document.getElementById('master-password');
if (masterPasswordInput) {
setTimeout(() => {
masterPasswordInput.focus();
}, 10);
}
} 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');
shortcuts.createShortcutProtectionManager();
}
} 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();
shortcuts.createShortcutProtectionManager();
} else {
shortcuts.createShortcutProtectionManager();
}
} else {
notifications.show('Please enter a valid password!', 'error');
}
}
});
}
shortcuts.updateAddShortcutModal();
const contextMenu = document.getElementById('context-menu');
if (contextMenu) {
contextMenu.addEventListener('click', (e) => {
const action = e.target.closest('.context-menu-item')?.dataset.action;
const index = parseInt(contextMenu.dataset.shortcutIndex);
if (action === 'edit') {
const currentShortcuts = Storage.get('shortcuts') || [];
const shortcut = currentShortcuts[index];
const modal = document.getElementById('edit-shortcut-modal');
if (modal) {
const urlInput = document.getElementById('edit-shortcut-url');
const nameInput = document.getElementById('edit-shortcut-name');
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');
const saveButton = document.getElementById('save-edit-shortcut');
const closeButton = document.getElementById('close-edit-shortcut');
const cancelButton = document.getElementById('cancel-edit-shortcut');
const closeModal = () => {
modal.classList.remove('active');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
};
const handleSave = () => {
const newUrl = urlInput.value.trim();
const newName = nameInput.value.trim();
if (newUrl && newName) {
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl);
if (formattedUrl) {
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');
}
}
};
saveButton.onclick = handleSave;
closeButton.onclick = closeModal;
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');
}
}
}
contextMenu.classList.add('hidden');
});
}
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 = '&times;';
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 = `
<label class="checkbox-label">
<input type="checkbox" id="protect-shortcut">
<span>Password protect this shortcut</span>
</label>
`;
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 = `
<label class="checkbox-label">
<input type="checkbox" id="protect-shortcut-edit">
<span>Password protect this shortcut</span>
</label>
`;
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();
}
};

36
js/storage.js Normal file
View File

@ -0,0 +1,36 @@
const Storage = {
get: (key) => {
try {
return JSON.parse(localStorage.getItem(key));
} catch (e) {
return null;
}
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
return false;
}
},
remove: (key) => {
try {
localStorage.removeItem(key);
return true;
} catch (e) {
return false;
}
},
clear: () => {
try {
localStorage.clear();
return true;
} catch (e) {
return false;
}
}
};

93
js/version.js Normal file
View File

@ -0,0 +1,93 @@
const versionUrl = 'https://www.junaid.xyz/projects/jstar-tab/version.txt';
const manifestVersion = chrome.runtime.getManifest().version;
function compareVersions(version1, version2) {
const v1 = version1.split('.').map(Number);
const v2 = version2.split('.').map(Number);
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
const diff = (v1[i] || 0) - (v2[i] || 0);
if (diff !== 0) return diff;
}
return 0;
}
async function checkForUpdate() {
try {
const response = await fetch(versionUrl, { cache: 'no-store' });
const latestVersion = await response.text();
handleVersionComparison(latestVersion);
} catch (error) {
const cachedResponse = await caches.match(versionUrl);
if (cachedResponse) {
const cachedVersion = await cachedResponse.text();
handleVersionComparison(cachedVersion, true);
} else {
updateVersionIcon(null);
}
}
}
function handleVersionComparison(latestVersion, isCached = false) {
latestVersion = latestVersion.trim();
const comparison = compareVersions(latestVersion, manifestVersion);
updateVersionIcon(comparison, latestVersion);
if (comparison > 0) {
const alertMessage = `New version ${latestVersion} available! ` +
`<a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/${latestVersion}" ` +
`target="_blank" style="color: #2196F3;">Update now</a>`;
if (isCached) {
notifications.show(`${alertMessage} (Showing cached version)`, 'info', 8000);
} else {
notifications.show(alertMessage, 'info');
}
}
}
function updateVersionIcon(versionComparison, latestVersion) {
const versionIcon = document.getElementById('version-icon');
if (!versionIcon) return;
versionIcon.className = 'version-icon fas';
versionIcon.style.color = '';
versionIcon.removeAttribute('title');
latestVersion = latestVersion.trim();
if (versionComparison === 0) {
versionIcon.classList.add('fa-check-circle');
versionIcon.style.color = '#4caf50';
versionIcon.title = 'Youre up to date! Enjoy the latest features.';
} else if (versionComparison > 0) {
versionIcon.classList.add('fa-exclamation-circle');
versionIcon.style.color = '#ff9800';
versionIcon.title = `A newer version (${latestVersion}) is available! Dont miss out on the new goodies.`;
} else if (versionComparison < 0) {
versionIcon.classList.add('fa-question-circle');
versionIcon.style.color = '#2196f3';
versionIcon.title = 'Whoa! Youre ahead of the curve. Are you from the future?';
} else {
versionIcon.classList.add('fa-times-circle');
versionIcon.style.color = '#f44336';
versionIcon.title = 'Unable to check the version. Is the internet sleeping?';
}
}
function showUpdateNotification(latestVersion) {
const message = `Version ${latestVersion} is available! <a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/${latestVersion}" target="_blank">Update now</a>!`;
notifications.show(message, 'info');
}
checkForUpdate();
document.addEventListener('DOMContentLoaded', () => {
const version = chrome.runtime.getManifest().version;
const versionElement = document.getElementById('extension-version');
if (versionElement) {
versionElement.innerHTML = `JSTAR Tab v<a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/${version}" target="_blank" style="color: inherit;">${version}</a> <span id="version-icon" class="version-icon"></span>`;
}
});

View File

@ -1,13 +1,36 @@
{
"manifest_version": 3,
"name": "JSTAR Tab",
"version": "1.0",
"description": "A sleek modern custom new tab with search, shortcuts, and settings.",
"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"],
"permissions": [
"storage",
"favicon",
"history"
],
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"action": {
"default_title": "New JSTAR Tab"
"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": ["<all_urls>"]
}]
}

380
script.js
View File

@ -1,380 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
applyTheme();
updateGreeting();
loadAnonymizationState(); // Load anonymization state on page load
loadShortcuts(); // Load shortcuts on page load
document.getElementById('theme-switch').addEventListener('change', toggleTheme);
document.getElementById('settings-btn').addEventListener('click', toggleSettings);
document.getElementById('search-btn').addEventListener('click', searchBrave);
document.getElementById('search').addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
searchBrave();
}
});
// Add event listener for the anonymize button
const anonymizeBtn = document.getElementById('anonymize-btn');
anonymizeBtn.addEventListener('click', toggleAnonymize);
const modal = document.getElementById('settings-modal');
modal.addEventListener('click', closeOnClickOutside);
document.querySelector('.close-btn').addEventListener('click', closeSettings);
// Add shortcut button listeners
document.getElementById('add-shortcut').addEventListener('click', addShortcut);
document.getElementById('import-shortcuts').addEventListener('click', importShortcuts);
document.getElementById('export-shortcuts').addEventListener('click', exportShortcuts);
document.getElementById('reset-shortcuts').addEventListener('click', resetShortcuts);
// Custom context menu for shortcuts
const contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.innerHTML = `
<button id="edit-shortcut">Edit</button>
<button id="delete-shortcut">Delete</button>
`;
contextMenu.style.display = 'none';
document.body.appendChild(contextMenu);
document.addEventListener('click', (event) => {
const contextMenu = document.getElementById('context-menu');
// Hide context menu if the click is outside of it
if (event.target !== contextMenu && !contextMenu.contains(event.target)) {
contextMenu.style.display = 'none'; // Hide menu on click outside
}
});
// Close context menu on Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
contextMenu.style.display = 'none'; // Hide context menu
}
});
// Detect Shift + A to toggle anonymization, but ensure no other keys are pressed
document.addEventListener('keydown', (event) => {
if (event.key === 'A' && event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
toggleAnonymize();
}
});
// Detect Shift + T to toggle theme
document.addEventListener('keydown', (event) => {
if (event.key === 'T' && event.shiftKey) {
toggleThemeShortcut(); // Toggle theme when Shift + T is pressed
}
});
// Detect Shift + S to toggle settings menu
document.addEventListener('keydown', (event) => {
if (event.key === 'S' && event.shiftKey) {
toggleSettings(); // Open or close settings modal
}
});
});
let isAnonymized = false; // Track the anonymization state
let currentShortcutIndex = -1; // To keep track of the current shortcut for editing or deleting
function updateGreeting() {
const greeting = document.getElementById('greeting');
const date = new Date();
const hours = date.getUTCHours() + 5; // Adjust for timezone
let message = `Good ${getTimeOfDay(hours)}, ${isAnonymized ? 'JSTAR' : 'Junaid'}!`;
greeting.textContent = message;
}
function getTimeOfDay(hours) {
if (hours >= 5 && hours < 12) {
return "Morning";
} else if (hours >= 12 && hours < 17) {
return "Afternoon";
} else if (hours >= 17 && hours < 22) {
return "Evening";
} else {
return "Night";
}
}
function searchBrave() {
const query = document.getElementById('search').value.trim();
if (query) {
window.location.href = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
}
}
function toggleTheme() {
const isDark = document.getElementById('theme-switch').checked;
document.body.classList.toggle('dark-theme', isDark);
localStorage.setItem('theme', isDark ? 'dark' : 'light');
const modalContent = document.querySelector('.modal-content');
modalContent.style.backgroundColor = isDark ? '#1a1a1a' : 'white';
}
// Shortcut to toggle theme
function toggleThemeShortcut() {
const themeSwitch = document.getElementById('theme-switch');
themeSwitch.checked = !themeSwitch.checked;
toggleTheme();
}
function applyTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.body.classList.toggle('dark-theme', savedTheme === 'dark');
document.getElementById('theme-switch').checked = savedTheme === 'dark';
document.querySelector('.modal-content').style.backgroundColor = savedTheme === 'dark' ? '#1a1a1a' : 'white';
}
// Load the anonymization state from localStorage
function loadAnonymizationState() {
const savedAnonymization = localStorage.getItem('anonymization');
isAnonymized = savedAnonymization === 'true'; // Convert string to boolean
updateAnonymizeButton();
updateGreeting();
}
// Load shortcuts from localStorage and display them
function loadShortcuts() {
const shortcutsContainer = document.getElementById('shortcuts');
shortcutsContainer.innerHTML = ''; // Clear existing shortcuts
const savedShortcuts = JSON.parse(localStorage.getItem('shortcuts')) || [];
savedShortcuts.forEach((shortcut, index) => {
const shortcutButton = document.createElement('div');
shortcutButton.className = 'shortcut';
const favicon = document.createElement('img');
favicon.src = `https://www.google.com/s2/favicons?domain=${new URL(shortcut.url).hostname}`; // Fetch favicon
const shortcutName = document.createElement('span');
shortcutName.textContent = shortcut.name.length > 10 ? shortcut.name.slice(0, 10) + '...' : shortcut.name; // Truncate long names
// Add a click event to open the shortcut link
shortcutButton.addEventListener('click', (event) => {
if (event.ctrlKey) {
window.open(shortcut.url, '_blank'); // Open in a new tab if Ctrl is held
} else {
window.location.href = shortcut.url; // Open in the same tab
}
});
// Add context menu for right-click
shortcutButton.addEventListener('contextmenu', (event) => {
event.preventDefault();
showContextMenu(event.clientX, event.clientY, index);
});
shortcutButton.appendChild(favicon);
shortcutButton.appendChild(shortcutName);
shortcutsContainer.appendChild(shortcutButton);
});
}
// Show custom context menu
function showContextMenu(x, y, index) {
currentShortcutIndex = index; // Store the index of the current shortcut
const contextMenu = document.getElementById('context-menu');
contextMenu.style.display = 'block';
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
// Clear previous event handlers (important to prevent multiple listeners)
const editButton = contextMenu.querySelector('#edit-shortcut');
const deleteButton = contextMenu.querySelector('#delete-shortcut');
if (editButton) {
editButton.onclick = () => {
editShortcut(); // Trigger edit shortcut
contextMenu.style.display = 'none'; // Hide context menu
};
} else {
const editBtn = document.createElement('button');
editBtn.id = 'edit-shortcut';
editBtn.textContent = 'Edit';
editBtn.onclick = () => {
editShortcut(); // Trigger edit shortcut
contextMenu.style.display = 'none'; // Hide context menu
};
contextMenu.appendChild(editBtn);
}
if (deleteButton) {
deleteButton.onclick = () => {
deleteShortcut(); // Trigger delete shortcut
contextMenu.style.display = 'none'; // Hide context menu
};
} else {
const deleteBtn = document.createElement('button');
deleteBtn.id = 'delete-shortcut';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => {
deleteShortcut(); // Trigger delete shortcut
contextMenu.style.display = 'none'; // Hide context menu
};
contextMenu.appendChild(deleteBtn);
}
}
// Toggle anonymization state
function toggleAnonymize() {
isAnonymized = !isAnonymized; // Toggle the anonymized state
updateAnonymizeButton();
updateGreeting();
// Save the current anonymization state to localStorage
localStorage.setItem('anonymization', isAnonymized);
}
function updateAnonymizeButton() {
const anonymizeBtn = document.getElementById('anonymize-btn');
if (isAnonymized) {
anonymizeBtn.classList.add('active');
anonymizeBtn.setAttribute('data-tooltip', 'Unanonymize');
anonymizeBtn.innerHTML = '<i class="fas fa-user"></i>'; // Change icon to un-anonymize
} else {
anonymizeBtn.classList.remove('active');
anonymizeBtn.setAttribute('data-tooltip', 'Anonymize');
anonymizeBtn.innerHTML = '<i class="fas fa-user-secret"></i>'; // Change icon to anonymize
}
}
function toggleSettings() {
const modal = document.getElementById('settings-modal');
modal.classList.contains('active') ? closeSettings() : openSettings();
}
function openSettings() {
const modal = document.getElementById('settings-modal');
modal.style.display = 'flex';
setTimeout(() => {
modal.classList.add('active');
modal.querySelector('.modal-content').classList.add('active');
}, 50);
}
function closeSettings() {
const modal = document.getElementById('settings-modal');
modal.querySelector('.modal-content').classList.remove('active');
modal.classList.remove('active');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
}
function closeOnClickOutside(event) {
const modal = document.getElementById('settings-modal');
if (event.target === modal) {
closeSettings();
}
}
// Validate if the URL is valid
function isValidURL(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
// Add a new shortcut
function addShortcut() {
const name = prompt("Enter the shortcut name:");
const url = prompt("Enter the shortcut URL:");
if (!name || !url || !isValidURL(url)) {
alert("Invalid input. Please provide a valid name and URL.");
return;
}
const shortcuts = JSON.parse(localStorage.getItem('shortcuts')) || [];
shortcuts.push({ name, url });
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
loadShortcuts();
}
// Edit the current shortcut
function editShortcut() {
if (currentShortcutIndex < 0) return;
const shortcuts = JSON.parse(localStorage.getItem('shortcuts'));
const currentShortcut = shortcuts[currentShortcutIndex];
const newName = prompt("Edit the shortcut name:", currentShortcut.name);
const newUrl = prompt("Edit the shortcut URL:", currentShortcut.url);
if (!newName || !newUrl || !isValidURL(newUrl)) {
alert("Invalid input. Please provide a valid name and URL.");
return;
}
shortcuts[currentShortcutIndex] = { name: newName, url: newUrl };
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
loadShortcuts();
}
// Delete the current shortcut
function deleteShortcut() {
if (currentShortcutIndex < 0) return;
const shortcuts = JSON.parse(localStorage.getItem('shortcuts'));
shortcuts.splice(currentShortcutIndex, 1);
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
loadShortcuts();
}
// Import shortcuts from a JSON file
function importShortcuts() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
try {
const shortcuts = JSON.parse(e.target.result);
if (Array.isArray(shortcuts)) {
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
loadShortcuts();
} else {
alert("Invalid file format. Please upload a valid JSON file.");
}
} catch (error) {
alert("Error parsing JSON. Please ensure the file is valid.");
}
};
reader.readAsText(file);
};
input.click();
}
// Export shortcuts to a JSON file
function exportShortcuts() {
const shortcuts = JSON.parse(localStorage.getItem('shortcuts')) || [];
const dataStr = JSON.stringify(shortcuts, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'shortcuts.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Reset shortcuts to the default state
function resetShortcuts() {
if (confirm("Are you sure you want to reset the shortcuts? This action cannot be undone.")) {
localStorage.removeItem('shortcuts');
loadShortcuts();
}
}

399
style.css
View File

@ -1,399 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', sans-serif;
background-color: #ffffff; /* Light mode background */
color: #000000; /* Light mode text color */
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#container {
text-align: center;
}
#greeting {
font-size: 36px;
font-weight: 600;
margin-bottom: 30px;
}
#search-bar {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
background-color: #f0f0f0; /* Light mode input background */
border-radius: 30px;
padding: 5px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
#search-bar input {
font-size: 18px;
padding: 10px;
width: 400px;
border: none;
border-radius: 20px;
outline: none;
background-color: transparent;
color: #000000; /* Light mode input text color */
}
#search-bar button {
font-size: 18px;
padding: 10px;
cursor: pointer;
border: none;
background-color: transparent;
color: #000; /* Light mode icon color */
}
.grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
}
.shortcut {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 10px;
background-color: #e0e0e0; /* Light mode button background */
transition: background-color 0.3s;
cursor: pointer;
}
.shortcut img {
width: 24px; /* Size of favicon */
height: 24px; /* Size of favicon */
}
.shortcut:hover {
background-color: #ccc; /* Light mode button hover background */
}
.settings-icon {
position: fixed;
bottom: 20px;
right: 20px;
background-color: transparent;
border: none;
font-size: 32px;
cursor: pointer;
color: #000000; /* Light mode settings icon color */
z-index: 10;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8); /* Modal background color */
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.5s ease;
}
.modal-content {
background-color: #1A1A1A; /* Dark mode modal background */
padding: 20px;
border-radius: 10px;
width: 400px;
position: relative;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.close-btn {
position: absolute;
right: 20px;
top: 20px;
cursor: pointer;
font-size: 24px;
color: #000000; /* Close button color in dark mode */
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc; /* Light mode toggle background */
transition: 0.4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white; /* Light mode toggle circle color */
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: rgb(100, 100, 100); /* Light mode toggle active color */
}
input:checked + .slider:before {
background-color: rgb(133, 133, 133);
transform: translateX(26px);
}
body.dark-theme {
background-color: #000000; /* Dark mode background */
color: #ffffff; /* Dark mode text color */
}
body.dark-theme #search-bar {
background-color: #111111; /* Dark mode input background */
}
body.dark-theme #search-bar input {
color: #ffffff; /* Dark mode input text color */
}
body.dark-theme .settings-icon {
color: #ffffff; /* Dark mode settings icon color */
}
body.dark-theme .modal-content {
background-color: #1A1A1A; /* Dark mode modal background */
color: #ffffff; /* Dark mode modal text color */
}
body.dark-theme .close-btn {
color: #ffffff;
}
body.dark-theme #search-bar button {
color: #ffffff; /* Dark mode button text color */
}
body.dark-theme #search-bar button:hover {
color: #ffffff; /* Dark mode button hover color */
}
.section {
margin: 20px 0;
}
h2, h3 {
margin-bottom: 10px;
color: #000000; /* Light mode heading color */
}
body.dark-theme h2, body.dark-theme h3 {
color: #ffffff; /* Dark mode heading color */
}
.button-group {
display: flex;
justify-content: flex-start;
gap: 10px;
}
.icon-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
border: none;
background-color: #e0e0e0; /* Light mode button background */
border-radius: 5px;
color: #000000; /* Light mode button text color */
cursor: pointer;
position: relative;
transition: background-color 0.3s;
font-size: 18px;
}
.icon-btn:hover {
background-color: #ccc; /* Light mode button hover background */
}
body.dark-theme .icon-btn {
background-color: #333333; /* Dark mode button background */
color: #ffffff; /* Dark mode button text color */
}
body.dark-theme .icon-btn:hover {
background-color: #444444; /* Dark mode button hover background */
}
.icon-btn[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 10px 15px;
border-radius: 5px;
white-space: nowrap;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease-in;
pointer-events: none;
z-index: 100;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.icon-btn[data-tooltip]:hover::after {
opacity: 1;
}
.modal.active {
display: flex;
opacity: 1;
}
.modal-content.active {
opacity: 1;
transform: translateY(0);
}
.modal-content.hide {
opacity: 0;
transform: translateY(-20px);
}
.modal.hide {
opacity: 0;
}
/* Additional styles for the shortcut buttons */
.shortcut {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 10px;
background-color: #e0e0e0; /* Light mode button background */
transition: background-color 0.3s;
cursor: pointer;
}
.shortcut img {
width: 24px; /* Size of favicon */
height: 24px; /* Size of favicon */
}
.shortcut:hover {
background-color: #ccc; /* Light mode button hover background */
}
/* Dark mode styles for shortcuts */
body.dark-theme .shortcut {
background-color: #444; /* Dark mode button background */
}
body.dark-theme .shortcut:hover {
background-color: #555; /* Dark mode button hover background */
}
/* Custom context menu styles */
#context-menu {
position: absolute;
background-color: #fff; /* Light mode menu background */
border-radius: 5px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
z-index: 1000;
padding: 0; /* Remove padding from menu itself */
display: none; /* Hidden by default */
width: 150px; /* Set width for the context menu */
}
.context-menu-item {
display: flex;
align-items: center;
padding: 10px; /* Increased padding for larger button size */
color: #000; /* Default text color */
cursor: pointer;
border-bottom: 1px solid #fff; /* Divider between items */
transition: background-color 0.3s;
width: 100%; /* Full width buttons */
text-align: left; /* Align text to the left */
}
.context-menu-item i {
margin-right: 8px; /* Space between icon and text */
font-size: 16px; /* Icon size */
}
.context-menu-item:last-child {
border-bottom: none; /* No divider after last item */
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
.context-menu-item:hover {
background-color: #f0f0f0; /* Menu item hover background */
}
.context-menu-item.delete {
color: #000; /* Set delete button text color to red */
}
.context-menu-item.delete:hover {
color: red; /* Set delete button text color to red */
}
body.dark-theme #context-menu {
background-color: #222; /* Dark mode menu background */
}
body.dark-theme .context-menu-item {
color: #ffffff; /* Dark mode menu text color */
border-bottom: 1px solid #222222;
}
body.dark-theme .context-menu-item.delete {
color: #fff; /* Dark mode delete text color */
}
body.dark-theme .context-menu-item.delete:hover {
color: red; /* Dark mode delete text color */
}
body.dark-theme .context-menu-item:hover {
background-color: #333; /* Dark mode item hover background */
}

60
sw.js Normal file
View File

@ -0,0 +1,60 @@
const CACHE_PREFIX = 'jstartab-cache';
const VERSION_URL = 'https://www.junaid.xyz/projects/jstar-tab/version.txt';
const STATIC_CACHE = 'jstartab-static';
self.addEventListener('install', (event) => {
self.skipWaiting();
event.waitUntil(
caches.open(STATIC_CACHE).then(cache => cache.add(VERSION_URL))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
self.addEventListener('message', async (event) => {
if (event.data.action === 'updateFavicons') {
const faviconUrls = event.data.urls;
const currentDate = new Date();
const cacheName = `${CACHE_PREFIX}-${currentDate.toISOString().split('T')[0]}`;
const cache = await caches.open(cacheName);
await cache.addAll(faviconUrls);
const keys = await caches.keys();
keys.forEach((key) => {
if (key.startsWith(CACHE_PREFIX) && key !== cacheName) {
caches.delete(key);
}
});
}
});
self.addEventListener('fetch', (event) => {
const request = event.request;
if (event.request.url.startsWith('https://www.google.com/s2/favicons')) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((fetchResponse) => {
const cacheCopy = fetchResponse.clone();
caches.open(`${CACHE_PREFIX}-${new Date().toISOString().split('T')[0]}`)
.then((cache) => cache.put(event.request, cacheCopy));
return fetchResponse;
});
})
);
}
if (request.url === VERSION_URL) {
event.respondWith(
caches.open(STATIC_CACHE).then(cache =>
fetch(request).then(networkResponse => {
cache.put(request, networkResponse.clone());
return networkResponse;
}).catch(() => cache.match(request))
)
);
}
});