Compare commits

...

43 Commits
2.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
33 changed files with 9819 additions and 1998 deletions

View File

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

100
README.md
View File

@ -1,66 +1,84 @@
# 🌟 JSTAR Tab v2.0.0 # 🌟 JSTAR Tab
Welcome to **JSTAR Tab v2.0.0**! This is a customizable new tab extension for your browser, designed to enhance your browsing experience with personalization options, shortcuts, and more. 🚀 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. 🚀
## 🎉 What's New in v2.0.0? Transform your browsing experience with custom greetings, motion control, themes, fonts, and more. 🎉
- **Improved Shortcuts**: Add, edit, and manage up to 12 shortcuts with ease. 🔗 ## ✨ Features
- **Enhanced Personalization**: Set your display name and toggle anonymous mode to hide your name and shortcuts. 🕵️‍♂️
- **Data Management**: Import, export, and reset your settings and shortcuts. 📦
- **Notifications**: Get instant feedback with our new notification system. 🔔
- **Onboarding Process**: A guided setup to personalize your experience right from the start. 🎓
## 📥 Installation - **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
To set up JSTAR Tab locally, follow these steps: Examples:
- "Hello {name}, it's {time} on {day}!"
- "{greeting}, {name}! Today is {date}"
- "Happy {day}, {name}!"
1. **Download the Latest Release**: [JSTAR Tab v2.0.0](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest) - **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. 2. **Extract the Files**: Unzip the downloaded file to a directory of your choice.
3. **Load the Extension**: 3. **Load the Extension**:
- Open your browser and navigate to the extensions page. - Open your browser and navigate to the extensions page.
- Enable "Developer mode" (usually a toggle in the top right corner). - Enable "Developer mode" (usually a toggle in the top right corner).
- Click "Load unpacked" and select the directory where you extracted the files. - Click "Load unpacked" and select the directory where you extracted the files.
## 🛠️ Features ## 🎨 Customizing Your Experience
- **Customizable Themes**: Choose between light and dark themes to suit your mood. 🌗 ### **Greeting Formats**
- **Personalized Greeting**: Set a custom greeting with your name. Personalize your greeting with dynamic tags listed above. Example:
- **Search Engine Selection**: Choose your preferred search engine from a variety of options. "Good {greeting}, {name}! It's {time} on {day}!"
- **Shortcut Management**: Easily add, edit, and remove shortcuts to your favorite sites.
- **Data Backup and Restore**: Export your settings and shortcuts to a file and import them back anytime.
- **Onboarding Process**: A step-by-step guide to help you set up your new tab with ease.
## 🎨 Customizing Your JSTAR Tab ### **Themes & Fonts**
1. **Onboarding**: When you first install the extension, you'll be guided through an onboarding process to set up your preferences, including your name and preferred search engine. - Toggle between light and dark themes from the settings panel.
2. **Themes**: Switch between light and dark themes using the theme toggle button in the settings. This changes the overall appearance of your new tab. - Select from various font options to match your style.
3. **Greeting**: Personalize your greeting by entering your name in the settings. This will display a friendly message each time you open a new tab.
4. **Shortcuts**: Add shortcuts to your most visited sites. Click the "+" button to add a new shortcut, enter the name and URL, and save. You can also edit or delete shortcuts by right-clicking on them.
5. **Search Engine**: Select your preferred search engine from the settings. This will be used for all searches performed from the new tab.
## 📚 Usage ### **Shortcuts & Backgrounds**
- Add shortcuts with the "+" button.
- Edit or delete shortcuts by right-clicking on them.
- Customize your background by uploading your favorite images.
- **Settings**: Click the settings icon ⚙️ to open the settings panel where you can customize your experience. ### **Keyboard Shortcuts**
- **Add Shortcuts**: Click the "+" button to add a new shortcut. Enter the name and URL, then save. Set up custom keybinds for quick actions like opening settings or switching themes.
- **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.
## 📄 License ## 📄 License
This project is licensed under the MIT License. See the [LICENSE](https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE) file for details. This project is licensed under the [MIT License](https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE).
## ❤️ Acknowledgments
- **Font Awesome**: For the beautiful icons used throughout the extension.
- **Google Fonts**: For the sleek and modern typography.
## 🌐 Connect with Us ## 🌐 Connect with Us
- **[Linktree](https://linktr.ee/jstarsdev)** - **[Linktree](https://linktr.ee/jstarsdev)**
- **[Patreon](https://patreon.com/jstarsdev)** - **[Patreon](https://patreon.com/jstarsdev)**
- **[Latest Release](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest)** - **[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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/secrecy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

1175
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;
}
}
};

View File

@ -1,8 +1,24 @@
// Update greeting based on time of day and user settings if ('serviceWorker' in navigator) {
function updateGreeting() { navigator.serviceWorker.register('/sw.js');
}
async function updateGreeting() {
const greeting = document.getElementById('greeting'); const greeting = document.getElementById('greeting');
if (!greeting) return; 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 hour = new Date().getHours();
const isAnonymous = Storage.get('anonymousMode') || false; const isAnonymous = Storage.get('anonymousMode') || false;
const userName = isAnonymous ? const userName = isAnonymous ?
@ -22,13 +38,12 @@ function updateGreeting() {
}, 100); }, 100);
} }
// Set up event listeners for modal interactions
function initModalHandlers() { function initModalHandlers() {
const modals = document.querySelectorAll('.modal'); const modals = document.querySelectorAll('.modal');
modals.forEach(modal => { modals.forEach(modal => {
modal.addEventListener('click', (e) => { modal.addEventListener('click', (e) => {
if (e.target === modal) { if (e.target === modal && !modal.classList.contains('onboarding-modal')) {
closeModal(modal); closeModal(modal);
} }
}); });
@ -39,19 +54,32 @@ function initModalHandlers() {
e.stopPropagation(); e.stopPropagation();
}); });
} }
document.querySelectorAll('.modal .close-button').forEach(button => {
button.addEventListener('click', () => {
const modal = button.closest('.modal');
if (modal) {
closeModal(modal);
}
});
});
}); });
} }
// Open modal with animation
function openModal(modal) { function openModal(modal) {
if (!modal) return; if (!modal) return;
const contextMenu = document.querySelector('.context-menu');
if (contextMenu) {
contextMenu.classList.add('hidden');
}
modal.classList.remove('hidden'); modal.classList.remove('hidden');
requestAnimationFrame(() => { requestAnimationFrame(() => {
modal.classList.add('active'); modal.classList.add('active');
}); });
} }
// Close modal with animation
function closeModal(modal) { function closeModal(modal) {
if (!modal) return; if (!modal) return;
modal.classList.remove('active'); modal.classList.remove('active');
@ -60,39 +88,58 @@ function closeModal(modal) {
}, 300); }, 300);
} }
// Initialize application
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Apply visibility settings if (typeof settings !== 'undefined' && typeof settings.updateVisibility === 'function') {
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { settings.updateVisibility();
const isVisible = Storage.get(`show_${element}`); } else {
if (isVisible === false) { ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
const elementNode = document.getElementById(element === 'search' ? 'search-container' : element); const isVisible = Storage.get(`show_${element}`);
if (elementNode) elementNode.style.display = 'none'; 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';
}
}
});
}
// Start onboarding or show main content
if (!Storage.get('onboardingComplete')) { if (!Storage.get('onboardingComplete')) {
onboarding.start(); onboarding.start();
} else { } else {
document.getElementById('main-content').classList.remove('hidden'); document.getElementById('main-content').classList.remove('hidden');
} }
// Initialize features
search.init(); search.init();
shortcuts.init(); shortcuts.init();
settings.init(); settings.init();
initModalHandlers(); initModalHandlers();
// Set up greeting
updateGreeting(); updateGreeting();
setInterval(updateGreeting, 60000); setInterval(updateGreeting, 60000);
// Settings button handler
const settingsButton = document.getElementById('settings-button'); const settingsButton = document.getElementById('settings-button');
const settingsModal = document.getElementById('settings-modal'); const settingsModal = document.getElementById('settings-modal');
settingsButton.addEventListener('click', () => { settingsButton.addEventListener('click', () => {
openModal(settingsModal); 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();
}
}
}
}); });

View File

@ -1,23 +1,19 @@
// Notification System Class
class NotificationSystem { class NotificationSystem {
constructor() { constructor() {
this.container = document.getElementById('notification-container'); this.container = document.getElementById('notification-container');
this.notifications = new Map(); this.notifications = new Map();
} }
// Display a new notification
show(message, type = 'info', duration = 3000) { show(message, type = 'info', duration = 3000) {
const id = Date.now().toString(); const id = Date.now().toString();
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.className = `notification notification-${type}`; notification.className = `notification notification-${type}`;
// Create notification elements
const icon = this.createIcon(type); const icon = this.createIcon(type);
const content = this.createContent(message); const content = this.createContent(message);
const closeBtn = this.createCloseButton(id); const closeBtn = this.createCloseButton(id);
const progress = this.createProgressBar(type); const progress = this.createProgressBar(type);
// Assemble notification
notification.appendChild(icon); notification.appendChild(icon);
notification.appendChild(content); notification.appendChild(content);
notification.appendChild(closeBtn); notification.appendChild(closeBtn);
@ -25,10 +21,8 @@ class NotificationSystem {
this.container.appendChild(notification); this.container.appendChild(notification);
// Set removal timer
setTimeout(() => this.remove(id), duration); setTimeout(() => this.remove(id), duration);
// Store notification reference
this.notifications.set(id, { this.notifications.set(id, {
element: notification, element: notification,
duration duration
@ -39,7 +33,6 @@ class NotificationSystem {
return id; return id;
} }
// Remove a notification
remove(id) { remove(id) {
const notification = this.notifications.get(id); const notification = this.notifications.get(id);
if (notification) { if (notification) {
@ -51,7 +44,6 @@ class NotificationSystem {
} }
} }
// Update progress bar
updateProgress(id) { updateProgress(id) {
const notification = this.notifications.get(id); const notification = this.notifications.get(id);
if (notification) { if (notification) {
@ -72,7 +64,6 @@ class NotificationSystem {
} }
} }
// Helper methods for creating notification elements
createIcon(type) { createIcon(type) {
const icon = document.createElement('i'); const icon = document.createElement('i');
switch(type) { switch(type) {
@ -96,7 +87,7 @@ class NotificationSystem {
createContent(message) { createContent(message) {
const content = document.createElement('div'); const content = document.createElement('div');
content.className = 'notification-content'; content.className = 'notification-content';
content.textContent = message; content.innerHTML = message;
return content; return content;
} }
@ -127,5 +118,4 @@ class NotificationSystem {
} }
} }
// Initialize the notification system
const notifications = new NotificationSystem(); const notifications = new NotificationSystem();

View File

@ -1,73 +1,337 @@
// Onboarding module
const onboarding = { const onboarding = {
// Check if onboarding is complete 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: () => { isComplete: () => {
return Storage.get('onboardingComplete') === true; return Storage.get('onboardingComplete') === true;
}, },
// Start the onboarding process
start: () => { start: () => {
const modal = document.getElementById('onboarding-modal'); const onboardingContainer = document.getElementById('onboarding-container');
const mainContent = document.getElementById('main-content'); const mainContent = document.getElementById('main-content');
const fileInput = document.getElementById('onboarding-import');
document.getElementById('notification-container').style.zIndex = "20000";
if (!onboarding.isComplete()) { if (!onboarding.isComplete()) {
modal.classList.remove('hidden'); document.body.style.overflow = 'hidden';
modal.classList.add('active'); onboardingContainer.classList.remove('hidden');
mainContent.classList.add('hidden');
document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(1)); onboarding.initProgressDots();
document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete); onboarding.setupEventListeners();
// Set up search engine selection const theme = Storage.get('theme') || 'light';
const engines = document.querySelectorAll('.search-engine-option'); document.body.setAttribute('data-theme', theme);
engines.forEach(engine => {
engine.addEventListener('click', () => { document.querySelectorAll('.step-ob').forEach(step => {
engines.forEach(e => e.classList.remove('selected')); if (step.dataset.step !== "1") {
engine.classList.add('selected'); 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 { } else {
modal.classList.add('hidden');
mainContent.classList.remove('hidden'); 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 = '';
}
}
});
}, },
// Move to the next step in onboarding setupEventListeners: () => {
nextStep: (currentStep) => { document.querySelectorAll('.option-card-ob').forEach(card => {
const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`); card.addEventListener('click', () => {
const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`); const step = card.closest('.step-ob');
const name = document.getElementById('user-name').value.trim(); const stepNumber = parseInt(step.dataset.step);
const cards = step.querySelectorAll('.option-card-ob');
const nextButton = document.getElementById('next-step');
if (!name) { if (card.dataset.action === 'import-data') {
notifications.show('Please enter your name!', 'error'); if (!card.classList.contains('selected-ob')) {
return; 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');
} }
Storage.set('userName', name); setTimeout(() => {
const targetStepEl = document.querySelector(`.step-ob[data-step="${step}"]`);
if (targetStepEl) {
targetStepEl.classList.add('active-ob');
}
currentStepEl.classList.add('hidden'); onboarding.currentStep = step;
nextStepEl.classList.remove('hidden');
nextStepEl.classList.add('visible'); 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 the onboarding process
complete: () => { complete: () => {
const selectedEngine = document.querySelector('.search-engine-option.selected'); const onboardingContainer = document.getElementById('onboarding-container');
if (!selectedEngine) {
notifications.show('Please select a search engine!', 'error');
return;
}
const searchEngine = selectedEngine.dataset.engine;
Storage.set('searchEngine', searchEngine);
Storage.set('onboardingComplete', true);
const modal = document.getElementById('onboarding-modal');
const mainContent = document.getElementById('main-content'); const mainContent = document.getElementById('main-content');
modal.classList.add('hidden'); if (!onboarding.settings.theme) onboarding.settings.theme = 'light';
mainContent.classList.remove('hidden'); if (!onboarding.settings.fontFamily) onboarding.settings.fontFamily = 'Inter';
notifications.show('Welcome to your new tab! 👋', 'success'); if (!onboarding.settings.searchEngine) onboarding.settings.searchEngine = 'google';
updateGreeting(); 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);
}
});

View File

@ -1,27 +1,37 @@
const search = { const search = {
// Supported search engines and their URLs
engines: { engines: {
google: 'https://www.google.com/search?q=', google: {
bing: 'https://www.bing.com/search?q=', url: 'https://www.google.com/search?q=',
duckduckgo: 'https://duckduckgo.com/?q=', icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=32',
brave: 'https://search.brave.com/search?q=', name: 'Google'
qwant: 'https://www.qwant.com/?q=', },
searxng: 'https://searx.org/search?q=' bing: {
}, url: 'https://www.bing.com/search?q=',
icon: 'https://www.google.com/s2/favicons?domain=bing.com&sz=32',
// Perform search using selected engine name: 'Bing'
perform: () => { },
const searchBar = document.getElementById('search-bar'); duckduckgo: {
const query = searchBar.value.trim(); url: 'https://duckduckgo.com/?q=',
const engine = Storage.get('searchEngine') || 'google'; icon: 'https://www.google.com/s2/favicons?domain=duckduckgo.com&sz=32',
name: 'DuckDuckGo'
if (query) { },
const searchUrl = search.engines[engine] + encodeURIComponent(query); brave: {
window.location.href = searchUrl; 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'
} }
}, },
// Initialize search functionality
init: () => { init: () => {
const searchBar = document.getElementById('search-bar'); const searchBar = document.getElementById('search-bar');
const searchButton = document.getElementById('search-button'); const searchButton = document.getElementById('search-button');
@ -34,7 +44,6 @@ const search = {
searchButton.addEventListener('click', search.perform); searchButton.addEventListener('click', search.perform);
// Global keyboard shortcut to focus search bar
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === '/' && if (e.key === '/' &&
!['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) &&
@ -43,5 +52,29 @@ const search = {
searchBar.focus(); 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();
});

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
const shortcuts = { const shortcuts = {
MAX_SHORTCUTS: 12, MAX_SHORTCUTS: 12,
// Validate and format URL
validateAndFormatUrl: (url) => { validateAndFormatUrl: (url) => {
if (!/^https?:\/\//i.test(url)) { if (!/^https?:\/\//i.test(url)) {
url = 'https://' + url; url = 'https://' + url;
@ -15,8 +14,7 @@ const shortcuts = {
} }
}, },
// Add new shortcut add: (url, name, isPasswordProtected = false) => {
add: (url, name) => {
const currentShortcuts = Storage.get('shortcuts') || []; const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
notifications.show('Maximum shortcuts limit reached!', 'error'); notifications.show('Maximum shortcuts limit reached!', 'error');
@ -29,30 +27,156 @@ const shortcuts = {
return; return;
} }
currentShortcuts.push({ url: formattedUrl, name }); currentShortcuts.push({
url: formattedUrl,
name,
isPasswordProtected: isPasswordProtected || false
});
Storage.set('shortcuts', currentShortcuts); Storage.set('shortcuts', currentShortcuts);
shortcuts.render(); shortcuts.render();
CacheUpdater.update();
}, },
// Remove shortcut
remove: (index) => { remove: (index) => {
const currentShortcuts = Storage.get('shortcuts') || []; const currentShortcuts = Storage.get('shortcuts') || [];
currentShortcuts.splice(index, 1); currentShortcuts.splice(index, 1);
Storage.set('shortcuts', currentShortcuts); Storage.set('shortcuts', currentShortcuts);
shortcuts.render(); shortcuts.render();
notifications.show('Shortcut removed!', 'success'); notifications.show('Shortcut removed!', 'success');
CacheUpdater.update();
}, },
// Edit existing shortcut showConfirmDialog: (title, message, onConfirm) => {
edit: (index, newUrl, newName) => { 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') || []; const currentShortcuts = Storage.get('shortcuts') || [];
currentShortcuts[index] = { url: newUrl, name: newName }; currentShortcuts[index] = {
url: newUrl,
name: newName,
isPasswordProtected: isPasswordProtected || false
};
Storage.set('shortcuts', currentShortcuts); Storage.set('shortcuts', currentShortcuts);
shortcuts.render(); shortcuts.render();
notifications.show('Shortcut updated!', 'success'); notifications.show('Shortcut updated!', 'success');
CacheUpdater.update();
}, },
// Show context menu for shortcut
showContextMenu: (e, index) => { showContextMenu: (e, index) => {
e.preventDefault(); e.preventDefault();
const menu = document.getElementById('context-menu'); const menu = document.getElementById('context-menu');
@ -75,7 +199,6 @@ const shortcuts = {
}, 0); }, 0);
}, },
// Render shortcuts grid
render: () => { render: () => {
const grid = document.getElementById('shortcuts-grid'); const grid = document.getElementById('shortcuts-grid');
const currentShortcuts = Storage.get('shortcuts') || []; const currentShortcuts = Storage.get('shortcuts') || [];
@ -85,11 +208,14 @@ const shortcuts = {
currentShortcuts.forEach((shortcut, index) => { currentShortcuts.forEach((shortcut, index) => {
const element = document.createElement('div'); const element = document.createElement('div');
element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`; element.className = `shortcut ${isAnonymous ? 'blurred' : ''} ${shortcut.isPasswordProtected ? 'password-protected' : ''}`;
element.dataset.index = index;
const icon = document.createElement('img'); const icon = document.createElement('img');
icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`; icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
icon.alt = shortcut.name; icon.alt = shortcut.name;
icon.draggable = false;
const name = document.createElement('span'); const name = document.createElement('span');
name.textContent = shortcut.name; name.textContent = shortcut.name;
@ -98,10 +224,44 @@ const shortcuts = {
element.appendChild(name); element.appendChild(name);
element.addEventListener('click', (e) => { element.addEventListener('click', (e) => {
if (e.ctrlKey) { if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) {
window.open(shortcut.url, '_blank'); if (shortcut.isPasswordProtected) {
} else { e.preventDefault();
window.location.href = shortcut.url; 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');
}
} }
}); });
@ -130,9 +290,30 @@ const shortcuts = {
}); });
}, },
// Initialize shortcuts functionality
init: () => { 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 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) { if (addShortcutButton) {
addShortcutButton.addEventListener('click', (e) => { addShortcutButton.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@ -143,7 +324,6 @@ const shortcuts = {
return; return;
} }
const modal = document.getElementById('add-shortcut-modal');
if (modal) { if (modal) {
modal.classList.remove('hidden'); modal.classList.remove('hidden');
modal.classList.add('active'); modal.classList.add('active');
@ -160,12 +340,18 @@ const shortcuts = {
if (url && name) { if (url && name) {
try { try {
new URL(url); new URL(url);
shortcuts.add(url, name);
const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false;
const passwordProtectCheckbox = document.getElementById('protect-shortcut');
const isPasswordProtected = isPasswordProtectionEnabled && passwordProtectCheckbox && passwordProtectCheckbox.checked;
shortcuts.add(url, name, isPasswordProtected);
modal.classList.remove('active'); modal.classList.remove('active');
setTimeout(() => { setTimeout(() => {
modal.classList.add('hidden'); modal.classList.add('hidden');
urlInput.value = ''; urlInput.value = '';
nameInput.value = ''; nameInput.value = '';
if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false;
}, 300); }, 300);
notifications.show('Shortcut added successfully!', 'success'); notifications.show('Shortcut added successfully!', 'success');
} catch (e) { } catch (e) {
@ -183,6 +369,8 @@ const shortcuts = {
modal.classList.add('hidden'); modal.classList.add('hidden');
urlInput.value = ''; urlInput.value = '';
nameInput.value = ''; nameInput.value = '';
const passwordProtectCheckbox = document.getElementById('protect-shortcut');
if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false;
}, 300); }, 300);
}; };
} }
@ -190,7 +378,120 @@ const shortcuts = {
}); });
} }
// Context menu actions 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'); const contextMenu = document.getElementById('context-menu');
if (contextMenu) { if (contextMenu) {
contextMenu.addEventListener('click', (e) => { contextMenu.addEventListener('click', (e) => {
@ -209,6 +510,11 @@ const shortcuts = {
urlInput.value = shortcut.url; urlInput.value = shortcut.url;
nameInput.value = shortcut.name; nameInput.value = shortcut.name;
const protectCheckbox = document.getElementById('protect-shortcut-edit');
if (protectCheckbox) {
protectCheckbox.checked = shortcut.isPasswordProtected || false;
}
modal.classList.remove('hidden'); modal.classList.remove('hidden');
modal.classList.add('active'); modal.classList.add('active');
@ -230,8 +536,14 @@ const shortcuts = {
if (newUrl && newName) { if (newUrl && newName) {
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl); const formattedUrl = shortcuts.validateAndFormatUrl(newUrl);
if (formattedUrl) { if (formattedUrl) {
shortcuts.edit(index, formattedUrl, newName); const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false;
const isPasswordProtected = isPasswordProtectionEnabled &&
protectCheckbox && protectCheckbox.checked;
shortcuts.edit(index, formattedUrl, newName, isPasswordProtected);
closeModal(); closeModal();
shortcuts.createShortcutProtectionManager();
} else { } else {
notifications.show('Invalid URL format!', 'error'); notifications.show('Invalid URL format!', 'error');
} }
@ -243,7 +555,30 @@ const shortcuts = {
cancelButton.onclick = closeModal; cancelButton.onclick = closeModal;
} }
} else if (action === 'delete') { } 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.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'); contextMenu.classList.add('hidden');
@ -251,5 +586,240 @@ const shortcuts = {
} }
shortcuts.render(); 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();
} }
}; };

View File

@ -1,9 +1,4 @@
/**
* Storage utility object for managing localStorage operations
* All methods handle JSON parsing/stringifying and error cases
*/
const Storage = { const Storage = {
// Retrieve and parse stored value
get: (key) => { get: (key) => {
try { try {
return JSON.parse(localStorage.getItem(key)); return JSON.parse(localStorage.getItem(key));
@ -12,7 +7,6 @@ const Storage = {
} }
}, },
// Store value as JSON string
set: (key, value) => { set: (key, value) => {
try { try {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
@ -22,7 +16,6 @@ const Storage = {
} }
}, },
// Delete specific key
remove: (key) => { remove: (key) => {
try { try {
localStorage.removeItem(key); localStorage.removeItem(key);
@ -32,7 +25,6 @@ const Storage = {
} }
}, },
// Remove all stored data
clear: () => { clear: () => {
try { try {
localStorage.clear(); localStorage.clear();

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,14 +1,15 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "JSTAR Tab", "name": "JSTAR Tab",
"version": "2.0.0", "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.", "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": { "chrome_url_overrides": {
"newtab": "index.html" "newtab": "index.html"
}, },
"permissions": [ "permissions": [
"storage", "storage",
"favicon" "favicon",
"history"
], ],
"icons": { "icons": {
"16": "images/icon16.png", "16": "images/icon16.png",
@ -25,7 +26,6 @@
}, },
"author": "JSTAR", "author": "JSTAR",
"homepage_url": "https://github.com/DevJSTAR/JSTAR-Tab", "homepage_url": "https://github.com/DevJSTAR/JSTAR-Tab",
"update_url": "https://github.com/DevJSTAR/JSTAR-Tab/releases/latest",
"web_accessible_resources": [{ "web_accessible_resources": [{
"resources": [ "resources": [
"fonts/*", "fonts/*",

699
style.css
View File

@ -1,699 +0,0 @@
/* Root variables for light theme */
:root {
--primary: #f5f5f5;
--primary-hover: #e0e0e0;
--background: #ffffff;
--surface: #fafafa;
--border: #eaeaea;
--text: #1a1a1a;
--text-secondary: #666666;
--shadow: rgba(0, 0, 0, 0.08);
--modal-backdrop: rgba(0, 0, 0, 0.5);
--scrollbar-thumb: #e0e0e0;
--scrollbar-track: #f5f5f5;
--modal-background: #ffffff;
}
/* Dark theme variables */
[data-theme="dark"] {
--primary: #1a1a1a;
--primary-hover: #2a2a2a;
--background: #000000;
--surface: #111111;
--border: #333333;
--text: #ffffff;
--text-secondary: #999999;
--shadow: rgba(0, 0, 0, 0.3);
--modal-backdrop: rgba(255, 255, 255, 0.1);
--scrollbar-thumb: #333333;
--scrollbar-track: #1a1a1a;
--modal-background: #1a1a1a;
}
/* Dark theme button styles */
[data-theme="dark"] .btn-primary {
background: var(--primary-hover);
color: var(--text);
}
[data-theme="dark"] .btn-primary:hover {
background: #3a3a3a;
}
/* Global styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', -apple-system, sans-serif;
}
body {
background: var(--background);
color: var(--text);
min-height: 100vh;
}
.hidden {
display: none !important;
}
/* Modal styles */
.modal {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
background: var(--modal-backdrop);
}
.modal-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.modal-actions button {
flex: 1;
}
.modal.active {
opacity: 1;
visibility: visible;
}
.modal-content {
background: var(--modal-background);
border-radius: 24px;
padding: 2rem;
width: 90%;
max-width: 480px;
transform: translateY(20px);
opacity: 0;
transition: all 0.3s ease;
box-shadow: 0 10px 25px var(--shadow);
}
/* Modal animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal.active .modal-content {
transform: translateY(0);
opacity: 1;
animation: modalSlideIn 0.3s ease forwards;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Form element styles */
select {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--border);
border-radius: 12px;
background: var(--surface);
color: var(--text);
font-size: 1rem;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23666666%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.4-12.8z%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 0.65em auto;
}
select:focus {
outline: none;
border-color: var(--text);
}
/* Step styles */
.step h2 {
font-size: 1.75rem;
margin-bottom: 1rem;
font-weight: 700;
}
.step p {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.input-group {
margin-bottom: 1.5rem;
}
input[type="text"] {
width: 100%;
padding: 1rem;
border: 2px solid var(--border);
border-radius: 12px;
background: var(--surface);
color: var(--text);
font-size: 1rem;
}
input[type="text"]:focus {
outline: none;
border-color: var(--text);
}
/* Main content styles */
.center-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3rem;
padding: 2rem;
position: relative;
}
#greeting {
font-size: 2rem;
font-weight: 700;
margin: 0;
opacity: 0;
animation: fadeIn 0.5s ease forwards;
height: 48px;
visibility: hidden;
}
#greeting:not([style*="visibility"]) {
visibility: visible;
}
.search-container {
width: 100%;
max-width: 640px;
height: 56px;
visibility: visible;
}
/* Search engine options */
.search-engine-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin: 2rem 0;
}
.search-engine-option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1.5rem;
border-radius: 16px;
background: var(--surface);
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px var(--shadow);
border: 2px solid transparent;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.search-engine-option:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow);
}
.search-engine-option.selected {
background: var(--surface);
border: 2px solid var(--border);
}
.search-engine-option img {
width: 32px;
height: 32px;
border-radius: 4px;
}
.search-engine-option span {
font-size: 0.875rem;
color: var(--text);
text-align: center;
}
/* Search bar styles */
.search-wrapper {
position: relative;
}
#search-bar {
width: 100%;
padding: 1.25rem 1.5rem;
border: none;
border-radius: 16px;
background: var(--surface);
color: var(--text);
font-size: 1rem;
box-shadow: 0 4px 24px var(--shadow);
}
.search-icon {
position: absolute;
right: 1.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text);
cursor: pointer;
padding: 0.5rem;
}
/* Shortcuts styles */
.shortcuts-container {
width: 100%;
max-width: 640px;
}
#add-shortcut {
width: 40px;
height: 40px;
border-radius: 50%;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
background: var(--primary);
color: var(--text);
border: none;
cursor: pointer;
transition: background 0.2s ease;
}
#add-shortcut:hover {
background: var(--primary-hover);
}
#shortcuts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.shortcut {
background: var(--surface);
border-radius: 12px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
box-shadow: 0 2px 10px var(--shadow);
}
.shortcut:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px var(--shadow);
}
.shortcut img {
width: 24px;
height: 24px;
border-radius: 6px;
}
.shortcut span {
font-size: 0.75rem;
text-align: center;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.shortcut.blurred {
filter: blur(4px);
opacity: 0.7;
}
.shortcut.blurred:hover {
filter: blur(0);
opacity: 1;
}
/* Settings styles */
.settings-button {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--surface);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: var(--text);
box-shadow: 0 4px 24px var(--shadow);
}
.settings-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px var(--shadow);
}
.settings-panel {
max-height: 80vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.settings-panel::-webkit-scrollbar {
width: 8px;
}
.settings-panel::-webkit-scrollbar-track {
background: var(--scrollbar-track);
border-radius: 0 24px 24px 0;
}
.settings-panel::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: 4px;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
}
.modal-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.btn-icon {
background: none;
border: none;
color: var(--text);
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
opacity: 0.6;
}
.btn-icon:hover {
opacity: 1;
}
.settings-section {
padding: 1.5rem 0;
border-bottom: 1px solid var(--border);
}
.settings-section:last-child {
border-bottom: none;
}
.settings-section h3 {
margin-bottom: 1.5rem;
font-weight: 600;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
}
.setting-label {
font-weight: 500;
}
.data-management-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
}
.btn-danger {
background: #dc3545 !important;
color: white !important;
}
.btn-danger:hover {
background: #c82333 !important;
}
/* Toggle switch styles */
.toggle {
position: relative;
display: inline-block;
width: 50px;
height: 26px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--border);
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: var(--background);
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--text);
}
input:checked + .toggle-slider:before {
transform: translateX(24px);
}
/* Button styles */
.btn-primary {
background: var(--primary);
color: var(--text);
border: none;
padding: 1rem 2rem;
border-radius: 12px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.btn-primary:hover {
background: var(--primary-hover);
}
/* Notification styles */
#notification-container {
position: fixed;
top: 1.5rem;
right: 1.5rem;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 1rem;
pointer-events: none;
}
.notification {
background: var(--surface);
color: var(--text);
padding: 1.25rem 1.5rem;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.1);
display: flex;
align-items: center;
gap: 1.25rem;
pointer-events: auto;
position: relative;
overflow: hidden;
animation: slideInRight 0.4s cubic-bezier(0.16, 1, 0.3, 1), fadeIn 0.4s ease;
max-width: 420px;
border: 1px solid rgba(var(--text-rgb), 0.1);
}
.notification-content {
flex: 1;
font-size: 0.95rem;
line-height: 1.5;
}
.notification-close {
background: none;
border: none;
color: var(--text);
cursor: pointer;
padding: 0.5rem;
opacity: 0.6;
}
.notification-close:hover {
opacity: 1;
background: rgba(var(--text-rgb), 0.1);
}
.notification-progress {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background: var(--primary);
opacity: 0.8;
transition: width 0.1s linear;
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* About section styles */
.about-content {
text-align: center;
color: var(--text-secondary);
padding: 1rem 0;
}
.about-content a {
color: var(--text);
text-decoration: none;
}
.about-content a:hover {
text-decoration: underline;
}
.about-content .version {
font-size: 1.1rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.5rem;
}
.about-content .description {
margin-bottom: 1rem;
}
.about-content .features {
font-size: 0.9rem;
margin-bottom: 1rem;
}
.about-content .copyright {
font-size: 0.8rem;
margin-top: 1rem;
}
.made-with {
margin-top: 0.5rem;
font-size: 0.875rem;
}
.made-with i {
color: #ff6b6b;
}
/* Context menu styles */
.context-menu {
position: fixed;
background: var(--surface);
border-radius: 8px;
padding: 0.5rem;
box-shadow: 0 2px 10px var(--shadow);
z-index: 1000;
}
.context-menu.hidden {
display: none;
}
.context-menu-item {
padding: 0.75rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
border-radius: 4px;
}
.context-menu-item:hover {
background: var(--primary);
}
.context-menu-item i {
width: 16px;
}
/* Hidden element styles */
.search-container.hidden,
#greeting.hidden,
#shortcuts-grid.hidden,
#add-shortcut.hidden {
visibility: hidden !important;
opacity: 0 !important;
position: absolute !important;
pointer-events: none !important;
}

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))
)
);
}
});