Compare commits
No commits in common. "main" and "1.0.0" have entirely different histories.
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 JSTAR
|
Copyright (c) 2024 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
|
@ -1,84 +1,58 @@
|
||||||
# 🌟 JSTAR Tab
|
# 🌟 JSTAR Tab v1.0.0
|
||||||
|
|
||||||
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. 🚀
|
Welcome to **JSTAR Tab v1.0.0**! This extension transforms your new tab into a personalized dashboard with shortcuts, a search bar, and customizable settings. 🚀
|
||||||
|
|
||||||
Transform your browsing experience with custom greetings, motion control, themes, fonts, and more. 🎉
|
## 📥 Installation
|
||||||
|
|
||||||
## ✨ Features
|
To set up JSTAR Tab locally, follow these steps:
|
||||||
|
|
||||||
- **Custom Greeting Formats**: Create personalized greetings using dynamic format tags:
|
1. **Download the Files**: Clone or download the repository to your local machine.
|
||||||
- `{name}`: Your display name
|
|
||||||
- `{greeting}`: Time-based greeting (e.g., Good morning)
|
|
||||||
- `{time}`: Current time
|
|
||||||
- `{date}`: Current date
|
|
||||||
- `{day}`: Day of the week
|
|
||||||
- `{month}`: Current month
|
|
||||||
- `{year}`: Current year
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- "Hello {name}, it's {time} on {day}!"
|
|
||||||
- "{greeting}, {name}! Today is {date}"
|
|
||||||
- "Happy {day}, {name}!"
|
|
||||||
|
|
||||||
- **Font Selection**: Choose from multiple fonts (Inter, Poppins, Roboto, Montserrat, Quicksand, and Comic Sans) to personalize your experience.
|
|
||||||
- **Custom Background Images**: Upload and set your favorite images as your new tab background.
|
|
||||||
- **Customizable Themes**: Switch between light and dark modes to suit your mood.
|
|
||||||
- **Motion Preferences**: Control animation intensity across five levels: Default Animations, Subtle Animations, Reduced Motion, Minimal Motion, and No Animations. Perfect for accessibility and performance preferences.
|
|
||||||
- **Privacy Settings**: Includes Anonymous Mode, password protection for individual shortcuts, and more to give you control over your experience.
|
|
||||||
- **Shortcut Management**: Add, edit, and remove shortcuts to your favorite websites with an improved grid layout system. Middle-click support allows opening shortcuts in a new tab.
|
|
||||||
- **History Page**: View and manage your shortcut browsing history from a dedicated page.
|
|
||||||
- **Search Engine Selection**: Choose your preferred search engine for quick and efficient browsing.
|
|
||||||
- **Keyboard Shortcuts**: Set up custom keybinds for various actions:
|
|
||||||
- Open settings
|
|
||||||
- Toggle anonymous mode
|
|
||||||
- Change themes
|
|
||||||
- Open history
|
|
||||||
- Redirect to a specific URL
|
|
||||||
- **Data Backup and Restore**: Export and import your settings and shortcuts effortlessly.
|
|
||||||
|
|
||||||
## 🌐 Getting Started
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
1. Visit [JSTAR Tab Website](https://jstartab.netlify.app).
|
|
||||||
2. Select your browser or mobile browser from the available options.
|
|
||||||
3. Follow the installation instructions specific to your browser.
|
|
||||||
|
|
||||||
### Alternative Manual Installation
|
|
||||||
|
|
||||||
1. **Download the Latest Release**: [JSTAR Tab GitHub Releases](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest).
|
|
||||||
2. **Extract the Files**: Unzip the downloaded file to a directory of your choice.
|
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.
|
||||||
|
|
||||||
## 🎨 Customizing Your Experience
|
## 🛠️ Features
|
||||||
|
|
||||||
### **Greeting Formats**
|
- **Customizable Themes**: Switch between light and dark themes to suit your mood. 🌗
|
||||||
Personalize your greeting with dynamic tags listed above. Example:
|
- **Personalized Greeting**: Displays a greeting based on the time of day and your anonymization settings.
|
||||||
"Good {greeting}, {name}! It's {time} on {day}!"
|
- **Search Bar**: Quickly search the web using Brave's search engine.
|
||||||
|
- **Shortcut Management**: Add, edit, and remove shortcuts to your favorite sites.
|
||||||
|
- **Settings Panel**: Access various settings including theme toggle and shortcut management.
|
||||||
|
- **Anonymization**: Toggle anonymization to hide your name in the greeting.
|
||||||
|
|
||||||
### **Themes & Fonts**
|
## 🎨 Customizing Your JSTAR Tab
|
||||||
- Toggle between light and dark themes from the settings panel.
|
|
||||||
- Select from various font options to match your style.
|
|
||||||
|
|
||||||
### **Shortcuts & Backgrounds**
|
1. **Themes**: Use the theme switch in the settings to toggle between light and dark modes.
|
||||||
- Add shortcuts with the "+" button.
|
|
||||||
- Edit or delete shortcuts by right-clicking on them.
|
|
||||||
- Customize your background by uploading your favorite images.
|
|
||||||
|
|
||||||
### **Keyboard Shortcuts**
|
2. **Greeting**: The greeting changes based on the time of day and whether anonymization is enabled.
|
||||||
Set up custom keybinds for quick actions like opening settings or switching themes.
|
|
||||||
|
3. **Shortcuts**:
|
||||||
|
- **Add**: Click the "Add Shortcut" button, enter a name and URL, and save.
|
||||||
|
- **Edit**: Right-click on a shortcut and select "Edit" to modify its details.
|
||||||
|
- **Delete**: Right-click on a shortcut and select "Delete" to remove it.
|
||||||
|
|
||||||
|
4. **Search Engine**: The search bar uses Brave's search engine by default.
|
||||||
|
|
||||||
|
## 📚 Usage
|
||||||
|
|
||||||
|
- **Settings**: Click the settings icon ⚙️ to open the settings panel where you can customize your experience.
|
||||||
|
- **Add Shortcuts**: Click the "+" button to add a new shortcut. Enter the URL and name, then save.
|
||||||
|
- **Edit Shortcuts**: Right-click on a shortcut to edit or delete it.
|
||||||
|
- **Theme Toggle**: Switch between light and dark themes using the theme toggle button.
|
||||||
|
|
||||||
|
## 📝 Contributing
|
||||||
|
|
||||||
|
We welcome contributions! If you have suggestions or improvements, feel free to fork the repository and submit a pull request. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
This project is licensed under the [MIT License](https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE).
|
This project is licensed under the [MIT License](https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE).
|
||||||
|
|
||||||
## 🌐 Connect with Us
|
## ❤️ Acknowledgments
|
||||||
|
|
||||||
- **[Linktree](https://linktr.ee/jstarsdev)**
|
- **Font Awesome**: For the beautiful icons used throughout the extension.
|
||||||
- **[Patreon](https://patreon.com/jstarsdev)**
|
- **Google Fonts**: For the sleek and modern typography.
|
||||||
- **[GitHub Releases](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest)**
|
|
||||||
|
|
||||||
Thank you for choosing JSTAR Tab! We hope you enjoy the seamless, personalized, and now even more **accessible** browsing experience it brings. 🚀
|
Thank you for using JSTAR Tab! We hope it makes your browsing experience more enjoyable and productive. 😊
|
||||||
|
|
567
css/history.css
|
@ -1,567 +0,0 @@
|
||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,474 +0,0 @@
|
||||||
.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
144
history.html
|
@ -1,144 +0,0 @@
|
||||||
<!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>
|
|
Before Width: | Height: | Size: 339 KiB |
Before Width: | Height: | Size: 648 KiB |
Before Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 235 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 584 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB |
1239
index.html
|
@ -1,159 +0,0 @@
|
||||||
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));
|
|
||||||
});
|
|
|
@ -1,86 +0,0 @@
|
||||||
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();
|
|
||||||
});
|
|
|
@ -1,343 +0,0 @@
|
||||||
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
|
@ -1,904 +0,0 @@
|
||||||
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
|
@ -1,313 +0,0 @@
|
||||||
const FORBIDDEN_KEYS = [
|
|
||||||
'Tab', 'CapsLock', 'Meta', 'ContextMenu',
|
|
||||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
|
||||||
'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', 'ScrollLock', 'Pause', 'NumLock',
|
|
||||||
'/'
|
|
||||||
];
|
|
||||||
|
|
||||||
const keybinds = {
|
|
||||||
bindings: {},
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.bindings = Storage.get('keybinds') || {};
|
|
||||||
|
|
||||||
this.setupDefaultKeybinds();
|
|
||||||
|
|
||||||
const urlInput = document.getElementById('keybind-url');
|
|
||||||
const urlComboInput = document.getElementById('keybind-url-combo');
|
|
||||||
const historyComboInput = document.getElementById('keybind-history-combo');
|
|
||||||
|
|
||||||
if (this.bindings.url) {
|
|
||||||
urlInput.value = this.bindings.url.url || '';
|
|
||||||
urlComboInput.value = this.bindings.url.keys || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.bindings.history && historyComboInput) {
|
|
||||||
historyComboInput.value = this.bindings.history.keys || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastSavedUrl = urlInput.value;
|
|
||||||
|
|
||||||
function isValidUrl(string) {
|
|
||||||
try {
|
|
||||||
const urlString = string.match(/^https?:\/\//) ? string : `https://${string}`;
|
|
||||||
new URL(urlString);
|
|
||||||
return true;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
urlInput.addEventListener('input', () => {
|
|
||||||
if (!this.bindings.url) {
|
|
||||||
this.bindings.url = { url: '', keys: '' };
|
|
||||||
}
|
|
||||||
this.bindings.url.url = urlInput.value;
|
|
||||||
Storage.set('keybinds', this.bindings);
|
|
||||||
});
|
|
||||||
|
|
||||||
urlInput.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
urlInput.blur();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
urlInput.addEventListener('blur', () => {
|
|
||||||
const currentUrl = urlInput.value.trim();
|
|
||||||
|
|
||||||
if (currentUrl === lastSavedUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUrl) {
|
|
||||||
if (isValidUrl(currentUrl)) {
|
|
||||||
lastSavedUrl = currentUrl;
|
|
||||||
notifications.show('URL saved.', 'success');
|
|
||||||
} else {
|
|
||||||
notifications.show('Please enter a valid URL.', 'error');
|
|
||||||
urlInput.value = lastSavedUrl;
|
|
||||||
this.bindings.url.url = lastSavedUrl;
|
|
||||||
Storage.set('keybinds', this.bindings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const keybindInputs = document.querySelectorAll('[id^="keybind-"]');
|
|
||||||
keybindInputs.forEach(input => {
|
|
||||||
if (input.id === 'keybind-url') {
|
|
||||||
const urlBinding = this.bindings['url'];
|
|
||||||
if (urlBinding && urlBinding.url) {
|
|
||||||
input.value = urlBinding.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
if (this.bindings['url']) {
|
|
||||||
this.bindings['url'].url = input.value;
|
|
||||||
Storage.set('keybinds', this.bindings);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = input.id.replace('keybind-url-combo', 'url').replace('keybind-', '');
|
|
||||||
if (this.bindings[action]) {
|
|
||||||
input.value = this.bindings[action].keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentKeys = new Set();
|
|
||||||
let isProcessingKeybind = false;
|
|
||||||
|
|
||||||
input.addEventListener('keydown', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
input.blur();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.ctrlKey) {
|
|
||||||
notifications.show('CTRL key combinations are not allowed.', 'error');
|
|
||||||
isProcessingKeybind = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FORBIDDEN_KEYS.includes(e.key)) {
|
|
||||||
notifications.show('This key cannot be used as a keybind.', 'error');
|
|
||||||
isProcessingKeybind = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isProcessingKeybind = false;
|
|
||||||
|
|
||||||
if (e.key !== 'Alt' && e.key !== 'Shift') {
|
|
||||||
currentKeys.add(e.key);
|
|
||||||
}
|
|
||||||
if (e.altKey) currentKeys.add('Alt');
|
|
||||||
if (e.shiftKey) currentKeys.add('Shift');
|
|
||||||
|
|
||||||
input.value = Array.from(currentKeys).join('+');
|
|
||||||
});
|
|
||||||
|
|
||||||
input.addEventListener('keyup', (e) => {
|
|
||||||
if (isProcessingKeybind) {
|
|
||||||
currentKeys.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Alt' || e.key === 'Shift') {
|
|
||||||
if (currentKeys.size === 1) {
|
|
||||||
notifications.show('Add another key with Alt or Shift.', 'error');
|
|
||||||
}
|
|
||||||
currentKeys.clear();
|
|
||||||
input.value = this.bindings[action]?.keys || '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const combo = Array.from(currentKeys).join('+');
|
|
||||||
|
|
||||||
if (!combo) return;
|
|
||||||
|
|
||||||
const duplicate = Object.entries(this.bindings).find(([key, value]) =>
|
|
||||||
value.keys === combo && key !== action
|
|
||||||
);
|
|
||||||
|
|
||||||
if (duplicate) {
|
|
||||||
notifications.show('This keybind is already in use.', 'error');
|
|
||||||
currentKeys.clear();
|
|
||||||
input.value = this.bindings[action]?.keys || '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bindings[action] = {
|
|
||||||
keys: combo,
|
|
||||||
url: action === 'url' ? document.getElementById('keybind-url').value : null
|
|
||||||
};
|
|
||||||
Storage.set('keybinds', this.bindings);
|
|
||||||
notifications.show('Keybind saved.', 'success');
|
|
||||||
});
|
|
||||||
|
|
||||||
input.addEventListener('blur', () => {
|
|
||||||
currentKeys.clear();
|
|
||||||
input.value = this.bindings[action]?.keys || '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.clear-keybind').forEach(button => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
const action = button.dataset.for;
|
|
||||||
const input = document.getElementById(`keybind-${action}-combo`) ||
|
|
||||||
document.getElementById(`keybind-${action}`);
|
|
||||||
|
|
||||||
input.value = '';
|
|
||||||
if (action === 'url') {
|
|
||||||
document.getElementById('keybind-url').value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.bindings[action];
|
|
||||||
Storage.set('keybinds', this.bindings);
|
|
||||||
notifications.show('Keybind removed.', 'success');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.target.tagName === 'INPUT' || !Storage.get('onboardingComplete')) return;
|
|
||||||
|
|
||||||
const keys = [];
|
|
||||||
if (e.altKey) keys.push('Alt');
|
|
||||||
if (e.shiftKey) keys.push('Shift');
|
|
||||||
if (e.key !== 'Alt' && e.key !== 'Shift') keys.push(e.key);
|
|
||||||
|
|
||||||
const combo = keys.join('+');
|
|
||||||
|
|
||||||
Object.entries(this.bindings).forEach(([action, binding]) => {
|
|
||||||
if (binding.keys === combo) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.executeAction(action, binding);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setupDefaultKeybinds() {
|
|
||||||
const defaultBindings = {
|
|
||||||
settings: { keys: 'Shift+S' },
|
|
||||||
anonymous: { keys: 'Shift+X' },
|
|
||||||
theme: { keys: 'Shift+T' },
|
|
||||||
history: { keys: 'Shift+H' },
|
|
||||||
url: { keys: 'Shift+Q', url: '' }
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(defaultBindings).forEach(([action, binding]) => {
|
|
||||||
if (!this.bindings[action]) {
|
|
||||||
this.bindings[action] = binding;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Storage.set('keybinds', this.bindings);
|
|
||||||
},
|
|
||||||
|
|
||||||
executeAction(action, binding) {
|
|
||||||
if (!Storage.get('onboardingComplete')) return;
|
|
||||||
|
|
||||||
const settingsPage = document.getElementById('settings-page');
|
|
||||||
const passwordDialog = document.getElementById('password-dialog');
|
|
||||||
|
|
||||||
if (passwordDialog && !passwordDialog.classList.contains('hidden')) {
|
|
||||||
const cancelBtn = document.getElementById('cancel-password');
|
|
||||||
if (cancelBtn) {
|
|
||||||
cancelBtn.click();
|
|
||||||
} else {
|
|
||||||
passwordDialog.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
|
||||||
passwordDialog.classList.add('hidden');
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === 'settings' && settingsPage.classList.contains('active')) {
|
|
||||||
settingsPage.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
|
||||||
settingsPage.classList.add('hidden');
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeModal = document.querySelector('.modal.active');
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'settings':
|
|
||||||
if (settingsPage.classList.contains('hidden')) {
|
|
||||||
settings.updateSettingsUI();
|
|
||||||
settingsPage.classList.remove('hidden');
|
|
||||||
setTimeout(() => {
|
|
||||||
settingsPage.classList.add('active');
|
|
||||||
}, 10);
|
|
||||||
} else {
|
|
||||||
settings.updateSettingsUI();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'add-shortcut':
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcutModal = document.getElementById('add-shortcut-modal');
|
|
||||||
if (shortcutModal === activeModal) {
|
|
||||||
closeModal(shortcutModal);
|
|
||||||
} else {
|
|
||||||
openModal(shortcutModal);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'anonymous':
|
|
||||||
const isAnonymous = Storage.get('anonymousMode') || false;
|
|
||||||
Storage.set('anonymousMode', !isAnonymous);
|
|
||||||
|
|
||||||
if (!isAnonymous) {
|
|
||||||
const randomName = anonymousNames.generate();
|
|
||||||
Storage.set('anonymousName', randomName);
|
|
||||||
} else {
|
|
||||||
Storage.remove('anonymousName');
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcuts.render();
|
|
||||||
updateGreeting();
|
|
||||||
break;
|
|
||||||
case 'theme':
|
|
||||||
settings.toggleTheme();
|
|
||||||
break;
|
|
||||||
case 'history':
|
|
||||||
window.location.href = 'history.html';
|
|
||||||
break;
|
|
||||||
case 'url':
|
|
||||||
if (binding.url) {
|
|
||||||
const url = binding.url;
|
|
||||||
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ?
|
|
||||||
url : `https://${url}`;
|
|
||||||
|
|
||||||
window.location.href = fullUrl;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
145
js/main.js
|
@ -1,145 +0,0 @@
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('/sw.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateGreeting() {
|
|
||||||
const greeting = document.getElementById('greeting');
|
|
||||||
if (!greeting) return;
|
|
||||||
|
|
||||||
const customFormat = Storage.get('customGreeting');
|
|
||||||
if (customFormat) {
|
|
||||||
const formattedGreeting = await settings.formatGreeting(customFormat);
|
|
||||||
if (formattedGreeting) {
|
|
||||||
greeting.textContent = formattedGreeting;
|
|
||||||
greeting.style.opacity = '0';
|
|
||||||
setTimeout(() => {
|
|
||||||
greeting.style.opacity = '1';
|
|
||||||
}, 100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hour = new Date().getHours();
|
|
||||||
const isAnonymous = Storage.get('anonymousMode') || false;
|
|
||||||
const userName = isAnonymous ?
|
|
||||||
(Storage.get('anonymousName') || anonymousNames.generate()) :
|
|
||||||
(Storage.get('userName') || 'Friend');
|
|
||||||
|
|
||||||
let timeGreeting = 'Hello';
|
|
||||||
if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning';
|
|
||||||
else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon';
|
|
||||||
else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening';
|
|
||||||
else timeGreeting = 'Good Night';
|
|
||||||
|
|
||||||
greeting.textContent = `${timeGreeting}, ${userName}!`;
|
|
||||||
greeting.style.opacity = '0';
|
|
||||||
setTimeout(() => {
|
|
||||||
greeting.style.opacity = '1';
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initModalHandlers() {
|
|
||||||
const modals = document.querySelectorAll('.modal');
|
|
||||||
|
|
||||||
modals.forEach(modal => {
|
|
||||||
modal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === modal && !modal.classList.contains('onboarding-modal')) {
|
|
||||||
closeModal(modal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalContent = modal.querySelector('.modal-content');
|
|
||||||
if (modalContent) {
|
|
||||||
modalContent.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('.modal .close-button').forEach(button => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
const modal = button.closest('.modal');
|
|
||||||
if (modal) {
|
|
||||||
closeModal(modal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openModal(modal) {
|
|
||||||
if (!modal) return;
|
|
||||||
|
|
||||||
const contextMenu = document.querySelector('.context-menu');
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
modal.classList.add('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal(modal) {
|
|
||||||
if (!modal) return;
|
|
||||||
modal.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (typeof settings !== 'undefined' && typeof settings.updateVisibility === 'function') {
|
|
||||||
settings.updateVisibility();
|
|
||||||
} else {
|
|
||||||
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
|
|
||||||
const isVisible = Storage.get(`show_${element}`);
|
|
||||||
if (isVisible === false) {
|
|
||||||
const elementNode = document.getElementById(element === 'search' ? 'search-container' :
|
|
||||||
element === 'addShortcut' ? 'add-shortcut' : element);
|
|
||||||
|
|
||||||
if (elementNode) {
|
|
||||||
elementNode.style.visibility = 'hidden';
|
|
||||||
elementNode.style.opacity = '0';
|
|
||||||
elementNode.style.position = 'absolute';
|
|
||||||
elementNode.style.pointerEvents = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Storage.get('onboardingComplete')) {
|
|
||||||
onboarding.start();
|
|
||||||
} else {
|
|
||||||
document.getElementById('main-content').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
search.init();
|
|
||||||
shortcuts.init();
|
|
||||||
settings.init();
|
|
||||||
initModalHandlers();
|
|
||||||
|
|
||||||
updateGreeting();
|
|
||||||
setInterval(updateGreeting, 60000);
|
|
||||||
|
|
||||||
const settingsButton = document.getElementById('settings-button');
|
|
||||||
const settingsModal = document.getElementById('settings-modal');
|
|
||||||
|
|
||||||
settingsButton.addEventListener('click', () => {
|
|
||||||
openModal(settingsModal);
|
|
||||||
});
|
|
||||||
|
|
||||||
keybinds.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const activeModal = document.querySelector('.modal.active');
|
|
||||||
if (activeModal && !activeModal.matches('#settings-modal')) {
|
|
||||||
const primaryButton = activeModal.querySelector('.btn-primary');
|
|
||||||
if (primaryButton) {
|
|
||||||
primaryButton.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,121 +0,0 @@
|
||||||
class NotificationSystem {
|
|
||||||
constructor() {
|
|
||||||
this.container = document.getElementById('notification-container');
|
|
||||||
this.notifications = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
show(message, type = 'info', duration = 3000) {
|
|
||||||
const id = Date.now().toString();
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = `notification notification-${type}`;
|
|
||||||
|
|
||||||
const icon = this.createIcon(type);
|
|
||||||
const content = this.createContent(message);
|
|
||||||
const closeBtn = this.createCloseButton(id);
|
|
||||||
const progress = this.createProgressBar(type);
|
|
||||||
|
|
||||||
notification.appendChild(icon);
|
|
||||||
notification.appendChild(content);
|
|
||||||
notification.appendChild(closeBtn);
|
|
||||||
notification.appendChild(progress);
|
|
||||||
|
|
||||||
this.container.appendChild(notification);
|
|
||||||
|
|
||||||
setTimeout(() => this.remove(id), duration);
|
|
||||||
|
|
||||||
this.notifications.set(id, {
|
|
||||||
element: notification,
|
|
||||||
duration
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateProgress(id);
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(id) {
|
|
||||||
const notification = this.notifications.get(id);
|
|
||||||
if (notification) {
|
|
||||||
notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)';
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.element.remove();
|
|
||||||
this.notifications.delete(id);
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgress(id) {
|
|
||||||
const notification = this.notifications.get(id);
|
|
||||||
if (notification) {
|
|
||||||
const progress = notification.element.querySelector('.notification-progress');
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const update = () => {
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const percent = 100 - (elapsed / notification.duration * 100);
|
|
||||||
|
|
||||||
if (percent > 0) {
|
|
||||||
progress.style.width = `${percent}%`;
|
|
||||||
requestAnimationFrame(update);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
requestAnimationFrame(update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcon(type) {
|
|
||||||
const icon = document.createElement('i');
|
|
||||||
switch(type) {
|
|
||||||
case 'success':
|
|
||||||
icon.className = 'fas fa-check-circle';
|
|
||||||
icon.style.color = 'var(--success-color, #4caf50)';
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
icon.className = 'fas fa-times-circle';
|
|
||||||
icon.style.color = 'var(--error-color, #f44336)';
|
|
||||||
break;
|
|
||||||
case 'info':
|
|
||||||
default:
|
|
||||||
icon.className = 'fas fa-info-circle';
|
|
||||||
icon.style.color = 'var(--info-color, #2196f3)';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
createContent(message) {
|
|
||||||
const content = document.createElement('div');
|
|
||||||
content.className = 'notification-content';
|
|
||||||
content.innerHTML = message;
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
createCloseButton(id) {
|
|
||||||
const closeBtn = document.createElement('button');
|
|
||||||
closeBtn.className = 'notification-close';
|
|
||||||
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
|
||||||
closeBtn.onclick = () => this.remove(id);
|
|
||||||
return closeBtn;
|
|
||||||
}
|
|
||||||
|
|
||||||
createProgressBar(type) {
|
|
||||||
const progress = document.createElement('div');
|
|
||||||
progress.className = 'notification-progress';
|
|
||||||
switch(type) {
|
|
||||||
case 'success':
|
|
||||||
progress.style.background = 'var(--success-color, #4caf50)';
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
progress.style.background = 'var(--error-color, #f44336)';
|
|
||||||
break;
|
|
||||||
case 'info':
|
|
||||||
default:
|
|
||||||
progress.style.background = 'var(--info-color, #2196f3)';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const notifications = new NotificationSystem();
|
|
337
js/onboarding.js
|
@ -1,337 +0,0 @@
|
||||||
const onboarding = {
|
|
||||||
currentStep: 1,
|
|
||||||
totalSteps: 5,
|
|
||||||
settings: {},
|
|
||||||
lastNotification: 0,
|
|
||||||
isCompleting: false,
|
|
||||||
notificationShown: false,
|
|
||||||
|
|
||||||
showNotification(message, type = 'info') {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastNotification >= 500) {
|
|
||||||
this.lastNotification = now;
|
|
||||||
notifications.show(message, type);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isComplete: () => {
|
|
||||||
return Storage.get('onboardingComplete') === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
start: () => {
|
|
||||||
const onboardingContainer = document.getElementById('onboarding-container');
|
|
||||||
const mainContent = document.getElementById('main-content');
|
|
||||||
const fileInput = document.getElementById('onboarding-import');
|
|
||||||
|
|
||||||
document.getElementById('notification-container').style.zIndex = "20000";
|
|
||||||
|
|
||||||
if (!onboarding.isComplete()) {
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
onboardingContainer.classList.remove('hidden');
|
|
||||||
|
|
||||||
onboarding.initProgressDots();
|
|
||||||
onboarding.setupEventListeners();
|
|
||||||
|
|
||||||
const theme = Storage.get('theme') || 'light';
|
|
||||||
document.body.setAttribute('data-theme', theme);
|
|
||||||
|
|
||||||
document.querySelectorAll('.step-ob').forEach(step => {
|
|
||||||
if (step.dataset.step !== "1") {
|
|
||||||
step.classList.remove('active-ob');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstStep = document.querySelector('.step-ob[data-step="1"]');
|
|
||||||
firstStep.classList.add('active-ob');
|
|
||||||
|
|
||||||
document.getElementById('prev-step').style.visibility = 'hidden';
|
|
||||||
|
|
||||||
const nextButton = document.getElementById('next-step');
|
|
||||||
nextButton.innerHTML = 'Next <svg><use href="#icon-arrow-right"/></svg>';
|
|
||||||
|
|
||||||
if (onboarding.currentStep > 1) {
|
|
||||||
nextButton.disabled = true;
|
|
||||||
nextButton.classList.add('disabled-ob');
|
|
||||||
}
|
|
||||||
|
|
||||||
mainContent.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
mainContent.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', async (e) => {
|
|
||||||
if (e.target.files.length > 0) {
|
|
||||||
try {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
const text = await file.text();
|
|
||||||
const data = JSON.parse(text);
|
|
||||||
|
|
||||||
if (!data.settings || !data.shortcuts || !Array.isArray(data.shortcuts)) {
|
|
||||||
throw new Error('Invalid data structure');
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(data.settings).forEach(([key, value]) => {
|
|
||||||
Storage.set(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
Storage.set('shortcuts', data.shortcuts);
|
|
||||||
|
|
||||||
if (data.keybinds) {
|
|
||||||
Storage.set('keybinds', data.keybinds);
|
|
||||||
}
|
|
||||||
|
|
||||||
Storage.set('onboardingComplete', true);
|
|
||||||
|
|
||||||
localStorage.setItem('showWelcomeAfterImport', 'true');
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
onboarding.showNotification('Failed to import data: Invalid file format!', 'error');
|
|
||||||
fileInput.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setupEventListeners: () => {
|
|
||||||
document.querySelectorAll('.option-card-ob').forEach(card => {
|
|
||||||
card.addEventListener('click', () => {
|
|
||||||
const step = card.closest('.step-ob');
|
|
||||||
const stepNumber = parseInt(step.dataset.step);
|
|
||||||
const cards = step.querySelectorAll('.option-card-ob');
|
|
||||||
const nextButton = document.getElementById('next-step');
|
|
||||||
|
|
||||||
if (card.dataset.action === 'import-data') {
|
|
||||||
if (!card.classList.contains('selected-ob')) {
|
|
||||||
document.getElementById('onboarding-import').click();
|
|
||||||
}
|
|
||||||
cards.forEach(c => c.classList.remove('selected-ob'));
|
|
||||||
card.classList.add('selected-ob');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cards.forEach(c => c.classList.remove('selected-ob'));
|
|
||||||
card.classList.add('selected-ob');
|
|
||||||
|
|
||||||
card.style.transform = 'scale(1.05)';
|
|
||||||
setTimeout(() => {
|
|
||||||
card.style.transform = 'scale(1.02)';
|
|
||||||
}, 150);
|
|
||||||
|
|
||||||
nextButton.disabled = false;
|
|
||||||
nextButton.classList.remove('disabled-ob');
|
|
||||||
|
|
||||||
if (card.dataset.theme) {
|
|
||||||
onboarding.settings.theme = card.dataset.theme;
|
|
||||||
document.body.setAttribute('data-theme', card.dataset.theme);
|
|
||||||
} else if (card.dataset.font) {
|
|
||||||
onboarding.settings.fontFamily = card.dataset.font;
|
|
||||||
document.documentElement.style.setProperty('--font-family', card.dataset.font);
|
|
||||||
} else if (card.dataset.engine) {
|
|
||||||
onboarding.settings.searchEngine = card.dataset.engine;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const nameInput = document.getElementById('user-name');
|
|
||||||
const nextButton = document.getElementById('next-step');
|
|
||||||
|
|
||||||
nameInput.addEventListener('input', (e) => {
|
|
||||||
const name = e.target.value.trim();
|
|
||||||
if (name) {
|
|
||||||
onboarding.settings.userName = name;
|
|
||||||
|
|
||||||
if (onboarding.currentStep === 4) {
|
|
||||||
nextButton.disabled = false;
|
|
||||||
nextButton.classList.remove('disabled-ob');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (onboarding.currentStep === 4) {
|
|
||||||
nextButton.disabled = true;
|
|
||||||
nextButton.classList.add('disabled-ob');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('prev-step').addEventListener('click', () => {
|
|
||||||
if (onboarding.currentStep > 1) {
|
|
||||||
onboarding.navigateToStep(onboarding.currentStep - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('next-step').addEventListener('click', () => {
|
|
||||||
let canProceed = true;
|
|
||||||
|
|
||||||
if (onboarding.currentStep === 2 && !onboarding.settings.theme) {
|
|
||||||
onboarding.showNotification('Please select a theme!', 'error');
|
|
||||||
canProceed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (onboarding.currentStep === 3 && !onboarding.settings.fontFamily) {
|
|
||||||
onboarding.showNotification('Please select a font!', 'error');
|
|
||||||
canProceed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (onboarding.currentStep === 4) {
|
|
||||||
const name = document.getElementById('user-name').value.trim();
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
onboarding.showNotification('Please enter your name!', 'error');
|
|
||||||
canProceed = false;
|
|
||||||
} else {
|
|
||||||
onboarding.settings.userName = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (onboarding.currentStep === 5 && !onboarding.settings.searchEngine) {
|
|
||||||
onboarding.showNotification('Please select a search engine!', 'error');
|
|
||||||
canProceed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canProceed) {
|
|
||||||
if (onboarding.currentStep < onboarding.totalSteps) {
|
|
||||||
onboarding.navigateToStep(onboarding.currentStep + 1);
|
|
||||||
} else {
|
|
||||||
onboarding.isCompleting = true;
|
|
||||||
localStorage.setItem('showWelcomeAfterImport', 'true');
|
|
||||||
onboarding.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.step-ob').forEach(step => {
|
|
||||||
if (step.dataset.step !== "1" && step.dataset.step !== "4") {
|
|
||||||
const firstOption = step.querySelector('.option-card-ob');
|
|
||||||
if (firstOption) {
|
|
||||||
setTimeout(() => {
|
|
||||||
firstOption.click();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
navigateToStep: (step) => {
|
|
||||||
const prevButton = document.getElementById('prev-step');
|
|
||||||
const nextButton = document.getElementById('next-step');
|
|
||||||
|
|
||||||
const currentStepEl = document.querySelector(`.step-ob[data-step="${onboarding.currentStep}"]`);
|
|
||||||
if (currentStepEl) {
|
|
||||||
currentStepEl.classList.remove('active-ob');
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const targetStepEl = document.querySelector(`.step-ob[data-step="${step}"]`);
|
|
||||||
if (targetStepEl) {
|
|
||||||
targetStepEl.classList.add('active-ob');
|
|
||||||
}
|
|
||||||
|
|
||||||
onboarding.currentStep = step;
|
|
||||||
|
|
||||||
prevButton.style.visibility = step === 1 ? 'hidden' : 'visible';
|
|
||||||
|
|
||||||
if (step === onboarding.totalSteps) {
|
|
||||||
nextButton.innerHTML = 'Get Started <svg><use href="#icon-sparkle"/></svg>';
|
|
||||||
} else {
|
|
||||||
nextButton.innerHTML = 'Next <svg><use href="#icon-arrow-right"/></svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((step === 2 && !onboarding.settings.theme) ||
|
|
||||||
(step === 3 && !onboarding.settings.fontFamily) ||
|
|
||||||
(step === 5 && !onboarding.settings.searchEngine)) {
|
|
||||||
nextButton.disabled = true;
|
|
||||||
nextButton.classList.add('disabled-ob');
|
|
||||||
} else if (step === 4) {
|
|
||||||
const name = document.getElementById('user-name').value.trim();
|
|
||||||
if (!name) {
|
|
||||||
nextButton.disabled = true;
|
|
||||||
nextButton.classList.add('disabled-ob');
|
|
||||||
} else {
|
|
||||||
nextButton.disabled = false;
|
|
||||||
nextButton.classList.remove('disabled-ob');
|
|
||||||
}
|
|
||||||
} else if (step === 1) {
|
|
||||||
nextButton.disabled = false;
|
|
||||||
nextButton.classList.remove('disabled-ob');
|
|
||||||
}
|
|
||||||
|
|
||||||
onboarding.updateProgressDots();
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
initProgressDots: () => {
|
|
||||||
const container = document.querySelector('.progress-dots-ob');
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < onboarding.totalSteps; i++) {
|
|
||||||
const dot = document.createElement('div');
|
|
||||||
dot.className = 'dot-ob' + (i === 0 ? ' active-ob' : '');
|
|
||||||
container.appendChild(dot);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateProgressDots: () => {
|
|
||||||
const dots = document.querySelectorAll('.dot-ob');
|
|
||||||
dots.forEach((dot, index) => {
|
|
||||||
dot.classList.toggle('active-ob', index + 1 === onboarding.currentStep);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
complete: () => {
|
|
||||||
const onboardingContainer = document.getElementById('onboarding-container');
|
|
||||||
const mainContent = document.getElementById('main-content');
|
|
||||||
|
|
||||||
if (!onboarding.settings.theme) onboarding.settings.theme = 'light';
|
|
||||||
if (!onboarding.settings.fontFamily) onboarding.settings.fontFamily = 'Inter';
|
|
||||||
if (!onboarding.settings.searchEngine) onboarding.settings.searchEngine = 'google';
|
|
||||||
if (!onboarding.settings.userName) onboarding.settings.userName = 'User';
|
|
||||||
|
|
||||||
document.body.setAttribute('data-theme', onboarding.settings.theme);
|
|
||||||
document.documentElement.style.setProperty('--font-family', onboarding.settings.fontFamily);
|
|
||||||
|
|
||||||
Object.entries(onboarding.settings).forEach(([key, value]) => {
|
|
||||||
Storage.set(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Storage.get('keybinds')) {
|
|
||||||
Storage.set('keybinds', {
|
|
||||||
settings: { keys: 'Shift+S' },
|
|
||||||
anonymous: { keys: 'Shift+X' },
|
|
||||||
theme: { keys: 'Shift+T' },
|
|
||||||
history: { keys: 'Shift+H' },
|
|
||||||
url: { keys: 'Shift+Q', url: '' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Storage.set('onboardingComplete', true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
onboardingContainer.classList.add('hidden');
|
|
||||||
mainContent.classList.remove('hidden');
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
|
|
||||||
search.init();
|
|
||||||
shortcuts.init();
|
|
||||||
settings.init();
|
|
||||||
updateGreeting();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!onboarding.notificationShown) {
|
|
||||||
onboarding.notificationShown = true;
|
|
||||||
onboarding.showNotification('Welcome to your new JSTAR Tab! 🎉', 'success');
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
onboarding.start();
|
|
||||||
|
|
||||||
if (onboarding.isComplete() && localStorage.getItem('showWelcomeAfterImport') === 'true') {
|
|
||||||
localStorage.removeItem('showWelcomeAfterImport');
|
|
||||||
setTimeout(() => {
|
|
||||||
notifications.show('Welcome to your new JSTAR Tab! 🎉', 'success');
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
});
|
|
80
js/search.js
|
@ -1,80 +0,0 @@
|
||||||
const search = {
|
|
||||||
engines: {
|
|
||||||
google: {
|
|
||||||
url: 'https://www.google.com/search?q=',
|
|
||||||
icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=32',
|
|
||||||
name: 'Google'
|
|
||||||
},
|
|
||||||
bing: {
|
|
||||||
url: 'https://www.bing.com/search?q=',
|
|
||||||
icon: 'https://www.google.com/s2/favicons?domain=bing.com&sz=32',
|
|
||||||
name: 'Bing'
|
|
||||||
},
|
|
||||||
duckduckgo: {
|
|
||||||
url: 'https://duckduckgo.com/?q=',
|
|
||||||
icon: 'https://www.google.com/s2/favicons?domain=duckduckgo.com&sz=32',
|
|
||||||
name: 'DuckDuckGo'
|
|
||||||
},
|
|
||||||
brave: {
|
|
||||||
url: 'https://search.brave.com/search?q=',
|
|
||||||
icon: 'https://www.google.com/s2/favicons?domain=brave.com&sz=32',
|
|
||||||
name: 'Brave'
|
|
||||||
},
|
|
||||||
qwant: {
|
|
||||||
url: 'https://www.qwant.com/?q=',
|
|
||||||
icon: 'https://www.google.com/s2/favicons?domain=qwant.com&sz=32',
|
|
||||||
name: 'Qwant'
|
|
||||||
},
|
|
||||||
searxng: {
|
|
||||||
url: 'https://searx.be/search?q=',
|
|
||||||
icon: 'https://www.google.com/s2/favicons?domain=searx.be&sz=32',
|
|
||||||
name: 'SearXNG'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
init: () => {
|
|
||||||
const searchBar = document.getElementById('search-bar');
|
|
||||||
const searchButton = document.getElementById('search-button');
|
|
||||||
|
|
||||||
searchBar.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
search.perform();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
searchButton.addEventListener('click', search.perform);
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === '/' &&
|
|
||||||
!['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) &&
|
|
||||||
window.getSelection().toString() === '') {
|
|
||||||
e.preventDefault();
|
|
||||||
searchBar.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchEngine = Storage.get('searchEngine') || 'google';
|
|
||||||
search.updateSearchEngineIcon(searchEngine);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSearchEngineIcon(engine) {
|
|
||||||
const searchIcon = document.querySelector('#search-container .search-icon img');
|
|
||||||
if (!searchIcon) return;
|
|
||||||
searchIcon.src = this.engines[engine].icon;
|
|
||||||
},
|
|
||||||
|
|
||||||
perform: () => {
|
|
||||||
const searchBar = document.getElementById('search-bar');
|
|
||||||
const query = searchBar.value.trim();
|
|
||||||
const engine = Storage.get('searchEngine') || 'google';
|
|
||||||
|
|
||||||
if (query) {
|
|
||||||
const searchUrl = search.engines[engine].url + encodeURIComponent(query);
|
|
||||||
window.location.href = searchUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
search.init();
|
|
||||||
});
|
|
1164
js/settings.js
825
js/shortcuts.js
|
@ -1,825 +0,0 @@
|
||||||
const shortcuts = {
|
|
||||||
MAX_SHORTCUTS: 12,
|
|
||||||
|
|
||||||
validateAndFormatUrl: (url) => {
|
|
||||||
if (!/^https?:\/\//i.test(url)) {
|
|
||||||
url = 'https://' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return url;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
add: (url, name, isPasswordProtected = false) => {
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
|
|
||||||
notifications.show('Maximum shortcuts limit reached!', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedUrl = shortcuts.validateAndFormatUrl(url);
|
|
||||||
if (!formattedUrl) {
|
|
||||||
notifications.show('Invalid URL format!', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentShortcuts.push({
|
|
||||||
url: formattedUrl,
|
|
||||||
name,
|
|
||||||
isPasswordProtected: isPasswordProtected || false
|
|
||||||
});
|
|
||||||
Storage.set('shortcuts', currentShortcuts);
|
|
||||||
shortcuts.render();
|
|
||||||
CacheUpdater.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
remove: (index) => {
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
currentShortcuts.splice(index, 1);
|
|
||||||
Storage.set('shortcuts', currentShortcuts);
|
|
||||||
shortcuts.render();
|
|
||||||
notifications.show('Shortcut removed!', 'success');
|
|
||||||
CacheUpdater.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
showConfirmDialog: (title, message, onConfirm) => {
|
|
||||||
const dialog = document.getElementById('confirmation-dialog');
|
|
||||||
const titleEl = document.getElementById('confirmation-title');
|
|
||||||
const messageEl = document.getElementById('confirmation-message');
|
|
||||||
const confirmBtn = document.getElementById('confirm-action');
|
|
||||||
const cancelBtn = document.getElementById('cancel-action');
|
|
||||||
|
|
||||||
titleEl.textContent = title;
|
|
||||||
messageEl.textContent = message;
|
|
||||||
|
|
||||||
dialog.classList.remove('hidden');
|
|
||||||
setTimeout(() => dialog.classList.add('active'), 10);
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
dialog.classList.remove('active');
|
|
||||||
setTimeout(() => dialog.classList.add('hidden'), 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
onConfirm();
|
|
||||||
closeDialog();
|
|
||||||
confirmBtn.removeEventListener('click', handleConfirm);
|
|
||||||
cancelBtn.removeEventListener('click', handleCancel);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
closeDialog();
|
|
||||||
confirmBtn.removeEventListener('click', handleConfirm);
|
|
||||||
cancelBtn.removeEventListener('click', handleCancel);
|
|
||||||
};
|
|
||||||
|
|
||||||
confirmBtn.addEventListener('click', handleConfirm);
|
|
||||||
cancelBtn.addEventListener('click', handleCancel);
|
|
||||||
},
|
|
||||||
|
|
||||||
showPasswordDialog: (shortcut, callback) => {
|
|
||||||
const dialog = document.getElementById('password-dialog');
|
|
||||||
const passwordInput = document.getElementById('shortcut-password');
|
|
||||||
const submitBtn = document.getElementById('submit-password');
|
|
||||||
const cancelBtn = document.getElementById('cancel-password');
|
|
||||||
const closeBtn = document.getElementById('close-password-dialog');
|
|
||||||
const errorMsg = document.getElementById('password-error');
|
|
||||||
const contextMenu = document.getElementById('context-menu');
|
|
||||||
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorMsg) {
|
|
||||||
errorMsg.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passwordInput) {
|
|
||||||
passwordInput.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialog) {
|
|
||||||
dialog.classList.remove('hidden');
|
|
||||||
setTimeout(() => {
|
|
||||||
dialog.classList.add('active');
|
|
||||||
if (passwordInput) {
|
|
||||||
passwordInput.focus();
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
dialog.classList.remove('active');
|
|
||||||
setTimeout(() => dialog.classList.add('hidden'), 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
const password = passwordInput.value;
|
|
||||||
const masterPassword = Storage.get('masterPassword');
|
|
||||||
|
|
||||||
if (!masterPassword) {
|
|
||||||
errorMsg.textContent = "No master password set. Please set one in settings.";
|
|
||||||
errorMsg.classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password === masterPassword) {
|
|
||||||
closeDialog();
|
|
||||||
callback();
|
|
||||||
submitBtn.removeEventListener('click', handleSubmit);
|
|
||||||
cancelBtn.removeEventListener('click', handleCancel);
|
|
||||||
closeBtn.removeEventListener('click', handleCancel);
|
|
||||||
passwordInput.removeEventListener('keydown', handleKeydown);
|
|
||||||
} else {
|
|
||||||
errorMsg.textContent = "Incorrect password. Please try again.";
|
|
||||||
errorMsg.classList.remove('hidden');
|
|
||||||
passwordInput.value = '';
|
|
||||||
passwordInput.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
closeDialog();
|
|
||||||
submitBtn.removeEventListener('click', handleSubmit);
|
|
||||||
cancelBtn.removeEventListener('click', handleCancel);
|
|
||||||
closeBtn.removeEventListener('click', handleCancel);
|
|
||||||
passwordInput.removeEventListener('keydown', handleKeydown);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeydown = (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleSubmit();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
handleCancel();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
submitBtn.addEventListener('click', handleSubmit);
|
|
||||||
cancelBtn.addEventListener('click', handleCancel);
|
|
||||||
closeBtn.addEventListener('click', handleCancel);
|
|
||||||
passwordInput.addEventListener('keydown', handleKeydown);
|
|
||||||
},
|
|
||||||
|
|
||||||
edit: (index, newUrl, newName, isPasswordProtected) => {
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
currentShortcuts[index] = {
|
|
||||||
url: newUrl,
|
|
||||||
name: newName,
|
|
||||||
isPasswordProtected: isPasswordProtected || false
|
|
||||||
};
|
|
||||||
Storage.set('shortcuts', currentShortcuts);
|
|
||||||
shortcuts.render();
|
|
||||||
notifications.show('Shortcut updated!', 'success');
|
|
||||||
CacheUpdater.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
showContextMenu: (e, index) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const menu = document.getElementById('context-menu');
|
|
||||||
const rect = e.target.getBoundingClientRect();
|
|
||||||
|
|
||||||
menu.style.top = `${e.clientY}px`;
|
|
||||||
menu.style.left = `${e.clientX}px`;
|
|
||||||
menu.classList.remove('hidden');
|
|
||||||
menu.dataset.shortcutIndex = index;
|
|
||||||
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (!menu.contains(event.target)) {
|
|
||||||
menu.classList.add('hidden');
|
|
||||||
document.removeEventListener('click', handleClickOutside);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', handleClickOutside);
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: () => {
|
|
||||||
const grid = document.getElementById('shortcuts-grid');
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
const isAnonymous = Storage.get('anonymousMode') || false;
|
|
||||||
|
|
||||||
grid.innerHTML = '';
|
|
||||||
|
|
||||||
currentShortcuts.forEach((shortcut, index) => {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
element.className = `shortcut ${isAnonymous ? 'blurred' : ''} ${shortcut.isPasswordProtected ? 'password-protected' : ''}`;
|
|
||||||
|
|
||||||
element.dataset.index = index;
|
|
||||||
|
|
||||||
const icon = document.createElement('img');
|
|
||||||
icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
|
|
||||||
icon.alt = shortcut.name;
|
|
||||||
icon.draggable = false;
|
|
||||||
|
|
||||||
const name = document.createElement('span');
|
|
||||||
name.textContent = shortcut.name;
|
|
||||||
|
|
||||||
element.appendChild(icon);
|
|
||||||
element.appendChild(name);
|
|
||||||
|
|
||||||
element.addEventListener('click', (e) => {
|
|
||||||
if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) {
|
|
||||||
if (shortcut.isPasswordProtected) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const openShortcut = () => {
|
|
||||||
if (e.ctrlKey || e.which === 2 || e.button === 1) {
|
|
||||||
window.open(shortcut.url, '_blank');
|
|
||||||
} else {
|
|
||||||
window.location.href = shortcut.url;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
shortcuts.showPasswordDialog(shortcut, openShortcut);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
if (e.ctrlKey || e.which === 2 || e.button === 1) {
|
|
||||||
window.open(shortcut.url, '_blank');
|
|
||||||
} else {
|
|
||||||
window.location.href = shortcut.url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
element.addEventListener('mousedown', (e) => {
|
|
||||||
if (e.button === 1) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (shortcut.isPasswordProtected) {
|
|
||||||
const openShortcut = () => {
|
|
||||||
window.open(shortcut.url, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
shortcuts.showPasswordDialog(shortcut, openShortcut);
|
|
||||||
} else {
|
|
||||||
window.open(shortcut.url, '_blank');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
element.addEventListener('contextmenu', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const menu = document.getElementById('context-menu');
|
|
||||||
|
|
||||||
menu.style.top = `${e.pageY}px`;
|
|
||||||
menu.style.left = `${e.pageX}px`;
|
|
||||||
menu.classList.remove('hidden');
|
|
||||||
menu.dataset.shortcutIndex = index;
|
|
||||||
|
|
||||||
const closeMenu = (event) => {
|
|
||||||
if (!menu.contains(event.target)) {
|
|
||||||
menu.classList.add('hidden');
|
|
||||||
document.removeEventListener('click', closeMenu);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', closeMenu);
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
grid.appendChild(element);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
init: () => {
|
|
||||||
const masterPasswordInput = document.getElementById('master-password');
|
|
||||||
if (masterPasswordInput) {
|
|
||||||
const savedPassword = Storage.get('masterPassword');
|
|
||||||
if (savedPassword) {
|
|
||||||
masterPasswordInput.value = savedPassword;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addShortcutButton = document.getElementById('add-shortcut');
|
|
||||||
const modal = document.getElementById('add-shortcut-modal');
|
|
||||||
const closeBtn = modal.querySelector('.close-modal');
|
|
||||||
|
|
||||||
if (closeBtn) {
|
|
||||||
closeBtn.addEventListener('click', () => {
|
|
||||||
modal.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
document.getElementById('shortcut-url').value = '';
|
|
||||||
document.getElementById('shortcut-name').value = '';
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addShortcutButton) {
|
|
||||||
addShortcutButton.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
|
|
||||||
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
|
|
||||||
notifications.show('Maximum shortcuts limit reached!', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modal) {
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
modal.classList.add('active');
|
|
||||||
|
|
||||||
const urlInput = document.getElementById('shortcut-url');
|
|
||||||
const nameInput = document.getElementById('shortcut-name');
|
|
||||||
|
|
||||||
const saveShortcutButton = document.getElementById('save-shortcut');
|
|
||||||
if (saveShortcutButton) {
|
|
||||||
saveShortcutButton.onclick = () => {
|
|
||||||
const url = urlInput.value.trim();
|
|
||||||
const name = nameInput.value.trim();
|
|
||||||
|
|
||||||
if (url && name) {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
|
|
||||||
const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false;
|
|
||||||
const passwordProtectCheckbox = document.getElementById('protect-shortcut');
|
|
||||||
const isPasswordProtected = isPasswordProtectionEnabled && passwordProtectCheckbox && passwordProtectCheckbox.checked;
|
|
||||||
|
|
||||||
shortcuts.add(url, name, isPasswordProtected);
|
|
||||||
modal.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
urlInput.value = '';
|
|
||||||
nameInput.value = '';
|
|
||||||
if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false;
|
|
||||||
}, 300);
|
|
||||||
notifications.show('Shortcut added successfully!', 'success');
|
|
||||||
} catch (e) {
|
|
||||||
notifications.show('Invalid URL format!', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelShortcutButton = document.getElementById('cancel-shortcut');
|
|
||||||
if (cancelShortcutButton) {
|
|
||||||
cancelShortcutButton.onclick = () => {
|
|
||||||
modal.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
urlInput.value = '';
|
|
||||||
nameInput.value = '';
|
|
||||||
const passwordProtectCheckbox = document.getElementById('protect-shortcut');
|
|
||||||
if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false;
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const anonymousTogglePrivacy = document.getElementById('toggle-anonymous-privacy');
|
|
||||||
if (anonymousTogglePrivacy) {
|
|
||||||
anonymousTogglePrivacy.checked = Storage.get('anonymousMode') || false;
|
|
||||||
|
|
||||||
anonymousTogglePrivacy.addEventListener('change', () => {
|
|
||||||
const anonymousToggle = document.getElementById('toggle-anonymous');
|
|
||||||
if (anonymousToggle) {
|
|
||||||
anonymousToggle.checked = anonymousTogglePrivacy.checked;
|
|
||||||
anonymousToggle.dispatchEvent(new Event('change'));
|
|
||||||
} else {
|
|
||||||
shortcuts.toggleAnonymousMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcutsPasswordToggle = document.getElementById('toggle-shortcuts-password');
|
|
||||||
if (shortcutsPasswordToggle) {
|
|
||||||
const isEnabled = Storage.get('passwordProtectionEnabled') || false;
|
|
||||||
shortcutsPasswordToggle.checked = isEnabled;
|
|
||||||
const passwordSettings = document.getElementById('password-protection-settings');
|
|
||||||
|
|
||||||
if (passwordSettings) {
|
|
||||||
if (isEnabled) {
|
|
||||||
passwordSettings.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
passwordSettings.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcutsPasswordToggle.addEventListener('change', () => {
|
|
||||||
const isEnabled = shortcutsPasswordToggle.checked;
|
|
||||||
Storage.set('passwordProtectionEnabled', isEnabled);
|
|
||||||
|
|
||||||
if (passwordSettings) {
|
|
||||||
if (isEnabled) {
|
|
||||||
passwordSettings.classList.remove('hidden');
|
|
||||||
const masterPasswordInput = document.getElementById('master-password');
|
|
||||||
if (masterPasswordInput) {
|
|
||||||
setTimeout(() => {
|
|
||||||
masterPasswordInput.focus();
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
passwordSettings.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcuts.updateAddShortcutModal();
|
|
||||||
|
|
||||||
if (isEnabled) {
|
|
||||||
const masterPassword = Storage.get('masterPassword');
|
|
||||||
|
|
||||||
if (!masterPassword) {
|
|
||||||
const masterPasswordInput = document.getElementById('master-password');
|
|
||||||
if (masterPasswordInput) {
|
|
||||||
masterPasswordInput.focus();
|
|
||||||
notifications.show('Please set a master password!', 'warning');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notifications.show('Password protection enabled!', 'success');
|
|
||||||
shortcuts.createShortcutProtectionManager();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
currentShortcuts.forEach(shortcut => {
|
|
||||||
shortcut.isPasswordProtected = false;
|
|
||||||
});
|
|
||||||
Storage.set('shortcuts', currentShortcuts);
|
|
||||||
shortcuts.render();
|
|
||||||
|
|
||||||
notifications.show('Password protection disabled!', 'info');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Storage.get('passwordProtectionEnabled')) {
|
|
||||||
shortcuts.createShortcutProtectionManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveMasterPasswordBtn = document.getElementById('save-master-password');
|
|
||||||
if (saveMasterPasswordBtn) {
|
|
||||||
saveMasterPasswordBtn.addEventListener('click', () => {
|
|
||||||
const masterPasswordInput = document.getElementById('master-password');
|
|
||||||
if (masterPasswordInput) {
|
|
||||||
const password = masterPasswordInput.value.trim();
|
|
||||||
|
|
||||||
if (password) {
|
|
||||||
Storage.set('masterPassword', password);
|
|
||||||
|
|
||||||
notifications.show('Master password updated!', 'success');
|
|
||||||
|
|
||||||
const shortcutsPasswordToggle = document.getElementById('toggle-shortcuts-password');
|
|
||||||
if (shortcutsPasswordToggle && !shortcutsPasswordToggle.checked) {
|
|
||||||
shortcutsPasswordToggle.checked = true;
|
|
||||||
Storage.set('passwordProtectionEnabled', true);
|
|
||||||
const passwordSettings = document.getElementById('password-protection-settings');
|
|
||||||
if (passwordSettings) {
|
|
||||||
passwordSettings.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
shortcuts.updateAddShortcutModal();
|
|
||||||
|
|
||||||
shortcuts.createShortcutProtectionManager();
|
|
||||||
} else {
|
|
||||||
shortcuts.createShortcutProtectionManager();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notifications.show('Please enter a valid password!', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcuts.updateAddShortcutModal();
|
|
||||||
|
|
||||||
const contextMenu = document.getElementById('context-menu');
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.addEventListener('click', (e) => {
|
|
||||||
const action = e.target.closest('.context-menu-item')?.dataset.action;
|
|
||||||
const index = parseInt(contextMenu.dataset.shortcutIndex);
|
|
||||||
|
|
||||||
if (action === 'edit') {
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
const shortcut = currentShortcuts[index];
|
|
||||||
const modal = document.getElementById('edit-shortcut-modal');
|
|
||||||
|
|
||||||
if (modal) {
|
|
||||||
const urlInput = document.getElementById('edit-shortcut-url');
|
|
||||||
const nameInput = document.getElementById('edit-shortcut-name');
|
|
||||||
|
|
||||||
urlInput.value = shortcut.url;
|
|
||||||
nameInput.value = shortcut.name;
|
|
||||||
|
|
||||||
const protectCheckbox = document.getElementById('protect-shortcut-edit');
|
|
||||||
if (protectCheckbox) {
|
|
||||||
protectCheckbox.checked = shortcut.isPasswordProtected || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
modal.classList.add('active');
|
|
||||||
|
|
||||||
const saveButton = document.getElementById('save-edit-shortcut');
|
|
||||||
const closeButton = document.getElementById('close-edit-shortcut');
|
|
||||||
const cancelButton = document.getElementById('cancel-edit-shortcut');
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
modal.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const newUrl = urlInput.value.trim();
|
|
||||||
const newName = nameInput.value.trim();
|
|
||||||
|
|
||||||
if (newUrl && newName) {
|
|
||||||
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl);
|
|
||||||
if (formattedUrl) {
|
|
||||||
const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false;
|
|
||||||
const isPasswordProtected = isPasswordProtectionEnabled &&
|
|
||||||
protectCheckbox && protectCheckbox.checked;
|
|
||||||
|
|
||||||
shortcuts.edit(index, formattedUrl, newName, isPasswordProtected);
|
|
||||||
closeModal();
|
|
||||||
|
|
||||||
shortcuts.createShortcutProtectionManager();
|
|
||||||
} else {
|
|
||||||
notifications.show('Invalid URL format!', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
saveButton.onclick = handleSave;
|
|
||||||
closeButton.onclick = closeModal;
|
|
||||||
cancelButton.onclick = closeModal;
|
|
||||||
}
|
|
||||||
} else if (action === 'delete') {
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
const shortcut = currentShortcuts[index];
|
|
||||||
|
|
||||||
shortcuts.showConfirmDialog(
|
|
||||||
'Delete Shortcut',
|
|
||||||
`Are you sure you want to delete "${shortcut.name}"?`,
|
|
||||||
() => {
|
|
||||||
shortcuts.remove(index);
|
|
||||||
shortcuts.createShortcutProtectionManager();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (action === 'open-new-tab') {
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
const shortcut = currentShortcuts[index];
|
|
||||||
|
|
||||||
if (shortcut && shortcut.url) {
|
|
||||||
if (shortcut.isPasswordProtected) {
|
|
||||||
shortcuts.showPasswordDialog(shortcut, () => {
|
|
||||||
window.open(shortcut.url, '_blank');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.open(shortcut.url, '_blank');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contextMenu.classList.add('hidden');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcuts.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
createShortcutProtectionManager: () => {
|
|
||||||
const passwordSettings = document.getElementById('password-protection-settings');
|
|
||||||
if (!passwordSettings) return;
|
|
||||||
|
|
||||||
let protectionManager = document.getElementById('shortcut-protection-manager');
|
|
||||||
if (!protectionManager) {
|
|
||||||
protectionManager = document.createElement('div');
|
|
||||||
protectionManager.id = 'shortcut-protection-manager';
|
|
||||||
protectionManager.className = 'shortcut-protection-manager';
|
|
||||||
|
|
||||||
const managerTitle = document.createElement('h4');
|
|
||||||
managerTitle.textContent = 'Protect Specific Shortcuts';
|
|
||||||
|
|
||||||
const managerDescription = document.createElement('p');
|
|
||||||
managerDescription.className = 'setting-description';
|
|
||||||
managerDescription.textContent = 'Select which shortcuts to password protect:';
|
|
||||||
|
|
||||||
protectionManager.appendChild(managerTitle);
|
|
||||||
protectionManager.appendChild(managerDescription);
|
|
||||||
|
|
||||||
passwordSettings.appendChild(protectionManager);
|
|
||||||
} else {
|
|
||||||
const children = Array.from(protectionManager.children);
|
|
||||||
children.forEach((child, index) => {
|
|
||||||
if (index > 1) protectionManager.removeChild(child);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
|
|
||||||
const selectedShortcutsContainer = document.createElement('div');
|
|
||||||
selectedShortcutsContainer.className = 'selected-shortcuts-container';
|
|
||||||
|
|
||||||
const protectedShortcuts = currentShortcuts.filter(shortcut => shortcut.isPasswordProtected);
|
|
||||||
|
|
||||||
if (protectedShortcuts.length > 0) {
|
|
||||||
protectedShortcuts.forEach((shortcut, index) => {
|
|
||||||
const shortcutChip = document.createElement('div');
|
|
||||||
shortcutChip.className = 'shortcut-chip';
|
|
||||||
shortcutChip.dataset.index = currentShortcuts.indexOf(shortcut);
|
|
||||||
|
|
||||||
const shortcutIcon = document.createElement('img');
|
|
||||||
shortcutIcon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
|
|
||||||
shortcutIcon.alt = shortcut.name;
|
|
||||||
|
|
||||||
const shortcutName = document.createElement('span');
|
|
||||||
shortcutName.textContent = shortcut.name;
|
|
||||||
|
|
||||||
const removeButton = document.createElement('button');
|
|
||||||
removeButton.className = 'remove-chip-btn';
|
|
||||||
removeButton.innerHTML = '×';
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,36 +0,0 @@
|
||||||
const Storage = {
|
|
||||||
get: (key) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(key));
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
set: (key, value) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
remove: (key) => {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clear: () => {
|
|
||||||
try {
|
|
||||||
localStorage.clear();
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,93 +0,0 @@
|
||||||
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 = 'You’re 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! Don’t miss out on the new goodies.`;
|
|
||||||
} else if (versionComparison < 0) {
|
|
||||||
versionIcon.classList.add('fa-question-circle');
|
|
||||||
versionIcon.style.color = '#2196f3';
|
|
||||||
versionIcon.title = 'Whoa! You’re 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>`;
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,36 +1,13 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "JSTAR Tab",
|
"name": "JSTAR Tab",
|
||||||
"version": "3.2.0",
|
"version": "1.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": "A sleek modern custom new tab with search, shortcuts, and settings.",
|
||||||
"chrome_url_overrides": {
|
"chrome_url_overrides": {
|
||||||
"newtab": "index.html"
|
"newtab": "index.html"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": ["storage"],
|
||||||
"storage",
|
|
||||||
"favicon",
|
|
||||||
"history"
|
|
||||||
],
|
|
||||||
"icons": {
|
|
||||||
"16": "images/icon16.png",
|
|
||||||
"48": "images/icon48.png",
|
|
||||||
"128": "images/icon128.png"
|
|
||||||
},
|
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "New JSTAR Tab",
|
"default_title": "New JSTAR Tab"
|
||||||
"default_icon": {
|
}
|
||||||
"16": "images/icon16.png",
|
|
||||||
"48": "images/icon48.png",
|
|
||||||
"128": "images/icon128.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"author": "JSTAR",
|
|
||||||
"homepage_url": "https://github.com/DevJSTAR/JSTAR-Tab",
|
|
||||||
"web_accessible_resources": [{
|
|
||||||
"resources": [
|
|
||||||
"fonts/*",
|
|
||||||
"images/*"
|
|
||||||
],
|
|
||||||
"matches": ["<all_urls>"]
|
|
||||||
}]
|
|
||||||
}
|
}
|
380
script.js
Normal file
|
@ -0,0 +1,380 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
applyTheme();
|
||||||
|
updateGreeting();
|
||||||
|
loadAnonymizationState(); // Load anonymization state on page load
|
||||||
|
loadShortcuts(); // Load shortcuts on page load
|
||||||
|
|
||||||
|
document.getElementById('theme-switch').addEventListener('change', toggleTheme);
|
||||||
|
document.getElementById('settings-btn').addEventListener('click', toggleSettings);
|
||||||
|
document.getElementById('search-btn').addEventListener('click', searchBrave);
|
||||||
|
document.getElementById('search').addEventListener('keypress', function(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
searchBrave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listener for the anonymize button
|
||||||
|
const anonymizeBtn = document.getElementById('anonymize-btn');
|
||||||
|
anonymizeBtn.addEventListener('click', toggleAnonymize);
|
||||||
|
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
modal.addEventListener('click', closeOnClickOutside);
|
||||||
|
document.querySelector('.close-btn').addEventListener('click', closeSettings);
|
||||||
|
|
||||||
|
// Add shortcut button listeners
|
||||||
|
document.getElementById('add-shortcut').addEventListener('click', addShortcut);
|
||||||
|
document.getElementById('import-shortcuts').addEventListener('click', importShortcuts);
|
||||||
|
document.getElementById('export-shortcuts').addEventListener('click', exportShortcuts);
|
||||||
|
document.getElementById('reset-shortcuts').addEventListener('click', resetShortcuts);
|
||||||
|
|
||||||
|
// Custom context menu for shortcuts
|
||||||
|
const contextMenu = document.createElement('div');
|
||||||
|
contextMenu.id = 'context-menu';
|
||||||
|
contextMenu.innerHTML = `
|
||||||
|
<button id="edit-shortcut">Edit</button>
|
||||||
|
<button id="delete-shortcut">Delete</button>
|
||||||
|
`;
|
||||||
|
contextMenu.style.display = 'none';
|
||||||
|
document.body.appendChild(contextMenu);
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const contextMenu = document.getElementById('context-menu');
|
||||||
|
// Hide context menu if the click is outside of it
|
||||||
|
if (event.target !== contextMenu && !contextMenu.contains(event.target)) {
|
||||||
|
contextMenu.style.display = 'none'; // Hide menu on click outside
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close context menu on Escape key
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
contextMenu.style.display = 'none'; // Hide context menu
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect Shift + A to toggle anonymization, but ensure no other keys are pressed
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'A' && event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||||
|
toggleAnonymize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect Shift + T to toggle theme
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'T' && event.shiftKey) {
|
||||||
|
toggleThemeShortcut(); // Toggle theme when Shift + T is pressed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect Shift + S to toggle settings menu
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'S' && event.shiftKey) {
|
||||||
|
toggleSettings(); // Open or close settings modal
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let isAnonymized = false; // Track the anonymization state
|
||||||
|
let currentShortcutIndex = -1; // To keep track of the current shortcut for editing or deleting
|
||||||
|
|
||||||
|
function updateGreeting() {
|
||||||
|
const greeting = document.getElementById('greeting');
|
||||||
|
const date = new Date();
|
||||||
|
const hours = date.getUTCHours() + 5; // Adjust for timezone
|
||||||
|
|
||||||
|
let message = `Good ${getTimeOfDay(hours)}, ${isAnonymized ? 'JSTAR' : 'Junaid'}!`;
|
||||||
|
greeting.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeOfDay(hours) {
|
||||||
|
if (hours >= 5 && hours < 12) {
|
||||||
|
return "Morning";
|
||||||
|
} else if (hours >= 12 && hours < 17) {
|
||||||
|
return "Afternoon";
|
||||||
|
} else if (hours >= 17 && hours < 22) {
|
||||||
|
return "Evening";
|
||||||
|
} else {
|
||||||
|
return "Night";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchBrave() {
|
||||||
|
const query = document.getElementById('search').value.trim();
|
||||||
|
if (query) {
|
||||||
|
window.location.href = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
const isDark = document.getElementById('theme-switch').checked;
|
||||||
|
document.body.classList.toggle('dark-theme', isDark);
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||||
|
|
||||||
|
const modalContent = document.querySelector('.modal-content');
|
||||||
|
modalContent.style.backgroundColor = isDark ? '#1a1a1a' : 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shortcut to toggle theme
|
||||||
|
function toggleThemeShortcut() {
|
||||||
|
const themeSwitch = document.getElementById('theme-switch');
|
||||||
|
themeSwitch.checked = !themeSwitch.checked;
|
||||||
|
toggleTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme() {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
document.body.classList.toggle('dark-theme', savedTheme === 'dark');
|
||||||
|
document.getElementById('theme-switch').checked = savedTheme === 'dark';
|
||||||
|
document.querySelector('.modal-content').style.backgroundColor = savedTheme === 'dark' ? '#1a1a1a' : 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the anonymization state from localStorage
|
||||||
|
function loadAnonymizationState() {
|
||||||
|
const savedAnonymization = localStorage.getItem('anonymization');
|
||||||
|
isAnonymized = savedAnonymization === 'true'; // Convert string to boolean
|
||||||
|
|
||||||
|
updateAnonymizeButton();
|
||||||
|
updateGreeting();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load shortcuts from localStorage and display them
|
||||||
|
function loadShortcuts() {
|
||||||
|
const shortcutsContainer = document.getElementById('shortcuts');
|
||||||
|
shortcutsContainer.innerHTML = ''; // Clear existing shortcuts
|
||||||
|
|
||||||
|
const savedShortcuts = JSON.parse(localStorage.getItem('shortcuts')) || [];
|
||||||
|
savedShortcuts.forEach((shortcut, index) => {
|
||||||
|
const shortcutButton = document.createElement('div');
|
||||||
|
shortcutButton.className = 'shortcut';
|
||||||
|
|
||||||
|
const favicon = document.createElement('img');
|
||||||
|
favicon.src = `https://www.google.com/s2/favicons?domain=${new URL(shortcut.url).hostname}`; // Fetch favicon
|
||||||
|
const shortcutName = document.createElement('span');
|
||||||
|
shortcutName.textContent = shortcut.name.length > 10 ? shortcut.name.slice(0, 10) + '...' : shortcut.name; // Truncate long names
|
||||||
|
|
||||||
|
// Add a click event to open the shortcut link
|
||||||
|
shortcutButton.addEventListener('click', (event) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
window.open(shortcut.url, '_blank'); // Open in a new tab if Ctrl is held
|
||||||
|
} else {
|
||||||
|
window.location.href = shortcut.url; // Open in the same tab
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add context menu for right-click
|
||||||
|
shortcutButton.addEventListener('contextmenu', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
showContextMenu(event.clientX, event.clientY, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
shortcutButton.appendChild(favicon);
|
||||||
|
shortcutButton.appendChild(shortcutName);
|
||||||
|
shortcutsContainer.appendChild(shortcutButton);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show custom context menu
|
||||||
|
function showContextMenu(x, y, index) {
|
||||||
|
currentShortcutIndex = index; // Store the index of the current shortcut
|
||||||
|
const contextMenu = document.getElementById('context-menu');
|
||||||
|
contextMenu.style.display = 'block';
|
||||||
|
contextMenu.style.left = `${x}px`;
|
||||||
|
contextMenu.style.top = `${y}px`;
|
||||||
|
|
||||||
|
// Clear previous event handlers (important to prevent multiple listeners)
|
||||||
|
const editButton = contextMenu.querySelector('#edit-shortcut');
|
||||||
|
const deleteButton = contextMenu.querySelector('#delete-shortcut');
|
||||||
|
|
||||||
|
if (editButton) {
|
||||||
|
editButton.onclick = () => {
|
||||||
|
editShortcut(); // Trigger edit shortcut
|
||||||
|
contextMenu.style.display = 'none'; // Hide context menu
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const editBtn = document.createElement('button');
|
||||||
|
editBtn.id = 'edit-shortcut';
|
||||||
|
editBtn.textContent = 'Edit';
|
||||||
|
editBtn.onclick = () => {
|
||||||
|
editShortcut(); // Trigger edit shortcut
|
||||||
|
contextMenu.style.display = 'none'; // Hide context menu
|
||||||
|
};
|
||||||
|
contextMenu.appendChild(editBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteButton) {
|
||||||
|
deleteButton.onclick = () => {
|
||||||
|
deleteShortcut(); // Trigger delete shortcut
|
||||||
|
contextMenu.style.display = 'none'; // Hide context menu
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.id = 'delete-shortcut';
|
||||||
|
deleteBtn.textContent = 'Delete';
|
||||||
|
deleteBtn.onclick = () => {
|
||||||
|
deleteShortcut(); // Trigger delete shortcut
|
||||||
|
contextMenu.style.display = 'none'; // Hide context menu
|
||||||
|
};
|
||||||
|
contextMenu.appendChild(deleteBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle anonymization state
|
||||||
|
function toggleAnonymize() {
|
||||||
|
isAnonymized = !isAnonymized; // Toggle the anonymized state
|
||||||
|
updateAnonymizeButton();
|
||||||
|
updateGreeting();
|
||||||
|
|
||||||
|
// Save the current anonymization state to localStorage
|
||||||
|
localStorage.setItem('anonymization', isAnonymized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAnonymizeButton() {
|
||||||
|
const anonymizeBtn = document.getElementById('anonymize-btn');
|
||||||
|
if (isAnonymized) {
|
||||||
|
anonymizeBtn.classList.add('active');
|
||||||
|
anonymizeBtn.setAttribute('data-tooltip', 'Unanonymize');
|
||||||
|
anonymizeBtn.innerHTML = '<i class="fas fa-user"></i>'; // Change icon to un-anonymize
|
||||||
|
} else {
|
||||||
|
anonymizeBtn.classList.remove('active');
|
||||||
|
anonymizeBtn.setAttribute('data-tooltip', 'Anonymize');
|
||||||
|
anonymizeBtn.innerHTML = '<i class="fas fa-user-secret"></i>'; // Change icon to anonymize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSettings() {
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
modal.classList.contains('active') ? closeSettings() : openSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSettings() {
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.add('active');
|
||||||
|
modal.querySelector('.modal-content').classList.add('active');
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettings() {
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
modal.querySelector('.modal-content').classList.remove('active');
|
||||||
|
modal.classList.remove('active');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOnClickOutside(event) {
|
||||||
|
const modal = document.getElementById('settings-modal');
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the URL is valid
|
||||||
|
function isValidURL(url) {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new shortcut
|
||||||
|
function addShortcut() {
|
||||||
|
const name = prompt("Enter the shortcut name:");
|
||||||
|
const url = prompt("Enter the shortcut URL:");
|
||||||
|
|
||||||
|
if (!name || !url || !isValidURL(url)) {
|
||||||
|
alert("Invalid input. Please provide a valid name and URL.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcuts = JSON.parse(localStorage.getItem('shortcuts')) || [];
|
||||||
|
shortcuts.push({ name, url });
|
||||||
|
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
|
||||||
|
loadShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit the current shortcut
|
||||||
|
function editShortcut() {
|
||||||
|
if (currentShortcutIndex < 0) return;
|
||||||
|
|
||||||
|
const shortcuts = JSON.parse(localStorage.getItem('shortcuts'));
|
||||||
|
const currentShortcut = shortcuts[currentShortcutIndex];
|
||||||
|
|
||||||
|
const newName = prompt("Edit the shortcut name:", currentShortcut.name);
|
||||||
|
const newUrl = prompt("Edit the shortcut URL:", currentShortcut.url);
|
||||||
|
|
||||||
|
if (!newName || !newUrl || !isValidURL(newUrl)) {
|
||||||
|
alert("Invalid input. Please provide a valid name and URL.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcuts[currentShortcutIndex] = { name: newName, url: newUrl };
|
||||||
|
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
|
||||||
|
loadShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the current shortcut
|
||||||
|
function deleteShortcut() {
|
||||||
|
if (currentShortcutIndex < 0) return;
|
||||||
|
|
||||||
|
const shortcuts = JSON.parse(localStorage.getItem('shortcuts'));
|
||||||
|
shortcuts.splice(currentShortcutIndex, 1);
|
||||||
|
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
|
||||||
|
loadShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import shortcuts from a JSON file
|
||||||
|
function importShortcuts() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
|
||||||
|
input.onchange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const shortcuts = JSON.parse(e.target.result);
|
||||||
|
if (Array.isArray(shortcuts)) {
|
||||||
|
localStorage.setItem('shortcuts', JSON.stringify(shortcuts));
|
||||||
|
loadShortcuts();
|
||||||
|
} else {
|
||||||
|
alert("Invalid file format. Please upload a valid JSON file.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert("Error parsing JSON. Please ensure the file is valid.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export shortcuts to a JSON file
|
||||||
|
function exportShortcuts() {
|
||||||
|
const shortcuts = JSON.parse(localStorage.getItem('shortcuts')) || [];
|
||||||
|
const dataStr = JSON.stringify(shortcuts, null, 2);
|
||||||
|
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'shortcuts.json';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset shortcuts to the default state
|
||||||
|
function resetShortcuts() {
|
||||||
|
if (confirm("Are you sure you want to reset the shortcuts? This action cannot be undone.")) {
|
||||||
|
localStorage.removeItem('shortcuts');
|
||||||
|
loadShortcuts();
|
||||||
|
}
|
||||||
|
}
|
399
style.css
Normal file
|
@ -0,0 +1,399 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
background-color: #ffffff; /* Light mode background */
|
||||||
|
color: #000000; /* Light mode text color */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#greeting {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #f0f0f0; /* Light mode input background */
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 5px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar input {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 400px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
outline: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #000000; /* Light mode input text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar button {
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #000; /* Light mode icon color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #e0e0e0; /* Light mode button background */
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut img {
|
||||||
|
width: 24px; /* Size of favicon */
|
||||||
|
height: 24px; /* Size of favicon */
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut:hover {
|
||||||
|
background-color: #ccc; /* Light mode button hover background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #000000; /* Light mode settings icon color */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8); /* Modal background color */
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #1A1A1A; /* Dark mode modal background */
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 400px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #000000; /* Close button color in dark mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc; /* Light mode toggle background */
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white; /* Light mode toggle circle color */
|
||||||
|
transition: 0.4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: rgb(100, 100, 100); /* Light mode toggle active color */
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
background-color: rgb(133, 133, 133);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme {
|
||||||
|
background-color: #000000; /* Dark mode background */
|
||||||
|
color: #ffffff; /* Dark mode text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme #search-bar {
|
||||||
|
background-color: #111111; /* Dark mode input background */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme #search-bar input {
|
||||||
|
color: #ffffff; /* Dark mode input text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .settings-icon {
|
||||||
|
color: #ffffff; /* Dark mode settings icon color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .modal-content {
|
||||||
|
background-color: #1A1A1A; /* Dark mode modal background */
|
||||||
|
color: #ffffff; /* Dark mode modal text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .close-btn {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme #search-bar button {
|
||||||
|
color: #ffffff; /* Dark mode button text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme #search-bar button:hover {
|
||||||
|
color: #ffffff; /* Dark mode button hover color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #000000; /* Light mode heading color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme h2, body.dark-theme h3 {
|
||||||
|
color: #ffffff; /* Dark mode heading color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px;
|
||||||
|
border: none;
|
||||||
|
background-color: #e0e0e0; /* Light mode button background */
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #000000; /* Light mode button text color */
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
background-color: #ccc; /* Light mode button hover background */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .icon-btn {
|
||||||
|
background-color: #333333; /* Dark mode button background */
|
||||||
|
color: #ffffff; /* Dark mode button text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .icon-btn:hover {
|
||||||
|
background-color: #444444; /* Dark mode button hover background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn[data-tooltip]::after {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn[data-tooltip]:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.active {
|
||||||
|
display: flex;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content.hide {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.hide {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional styles for the shortcut buttons */
|
||||||
|
.shortcut {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #e0e0e0; /* Light mode button background */
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut img {
|
||||||
|
width: 24px; /* Size of favicon */
|
||||||
|
height: 24px; /* Size of favicon */
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut:hover {
|
||||||
|
background-color: #ccc; /* Light mode button hover background */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles for shortcuts */
|
||||||
|
body.dark-theme .shortcut {
|
||||||
|
background-color: #444; /* Dark mode button background */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .shortcut:hover {
|
||||||
|
background-color: #555; /* Dark mode button hover background */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom context menu styles */
|
||||||
|
#context-menu {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #fff; /* Light mode menu background */
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 0; /* Remove padding from menu itself */
|
||||||
|
display: none; /* Hidden by default */
|
||||||
|
width: 150px; /* Set width for the context menu */
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px; /* Increased padding for larger button size */
|
||||||
|
color: #000; /* Default text color */
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #fff; /* Divider between items */
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
width: 100%; /* Full width buttons */
|
||||||
|
text-align: left; /* Align text to the left */
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item i {
|
||||||
|
margin-right: 8px; /* Space between icon and text */
|
||||||
|
font-size: 16px; /* Icon size */
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:last-child {
|
||||||
|
border-bottom: none; /* No divider after last item */
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background-color: #f0f0f0; /* Menu item hover background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.delete {
|
||||||
|
color: #000; /* Set delete button text color to red */
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.delete:hover {
|
||||||
|
color: red; /* Set delete button text color to red */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme #context-menu {
|
||||||
|
background-color: #222; /* Dark mode menu background */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .context-menu-item {
|
||||||
|
color: #ffffff; /* Dark mode menu text color */
|
||||||
|
border-bottom: 1px solid #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .context-menu-item.delete {
|
||||||
|
color: #fff; /* Dark mode delete text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .context-menu-item.delete:hover {
|
||||||
|
color: red; /* Dark mode delete text color */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme .context-menu-item:hover {
|
||||||
|
background-color: #333; /* Dark mode item hover background */
|
||||||
|
}
|
60
sw.js
|
@ -1,60 +0,0 @@
|
||||||
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))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|