Переглянути джерело

This is a mirror from `https://github.com/6a/vimeo-ripper` for personal archival
reasons, no content has been changed.

mitch donaberger 3 місяців тому
коміт
fc9cbf8f3c

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+package-lock.json
+node_modules

+ 317 - 0
README.md

@@ -0,0 +1,317 @@
+# 🎥 Vimeo Ripper (vget)
+
+A sleek, web-based Vimeo video downloader that extracts direct download links for Vimeo videos in multiple quality formats. Built with vanilla JavaScript and powered by Netlify Lambda functions.
+
+![Vimeo Ripper Interface](https://img.shields.io/badge/Interface-Web%20Based-blue)
+![JavaScript](https://img.shields.io/badge/JavaScript-ES6-yellow)
+![Netlify](https://img.shields.io/badge/Deployment-Netlify-00AD9F)
+![License](https://img.shields.io/badge/License-ISC-green)
+
+## ✨ Features
+
+- **🎯 Simple Interface**: Clean, intuitive web interface for easy video URL input
+- **📱 Responsive Design**: Works seamlessly across desktop and mobile devices
+- **🎬 Multiple Quality Options**: Download videos in 360p, 540p, 720p, or 1080p
+- **🖼️ Poster Extraction**: Get video thumbnail/poster images alongside video files
+- **⚡ Real-time Validation**: Instant URL validation with visual feedback
+- **🚀 Serverless Backend**: Fast, scalable processing using Netlify Lambda functions
+- **🔗 Direct Download Links**: No intermediary downloads - direct access to video files
+
+## 🛠️ Tech Stack
+
+- **Frontend**: HTML5, CSS3, Vanilla JavaScript
+- **Backend**: Node.js, Netlify Lambda Functions
+- **HTTP Client**: Axios
+- **Deployment**: Netlify
+- **APIs**: Vimeo Player Config API
+
+## 📋 Prerequisites
+
+Before running this project, make sure you have:
+
+- **Node.js** (version 12 or higher)
+- **npm** (comes with Node.js)
+- A modern web browser
+- Internet connection (for Vimeo API access)
+
+## 🚀 Installation
+
+### 1. Clone the Repository
+
+```bash
+git clone https://github.com/6a/vimeo-ripper.git
+cd vimeo-ripper
+```
+
+### 2. Install Dependencies
+
+```bash
+npm install
+```
+
+This will install the required packages:
+- `axios` - HTTP client for API requests
+- `netlify-lambda` - Local development and build tools for Lambda functions
+- `node-fetch` - Node.js implementation of the Fetch API
+
+## 💻 Development Setup
+
+### Local Development
+
+#### Option 1: Full Local Setup (Recommended)
+
+1. **Start the Lambda Function Server**:
+   ```bash
+   npm run start:lambda
+   ```
+   This starts the Netlify Lambda development server at `http://localhost:9000`
+
+2. **Serve the Frontend**:
+   In a new terminal window:
+   
+   ```bash
+   # Using Python (if available)
+   python3 -m http.server 8080
+   
+   # OR using Node.js http-server
+   npx http-server -p 8080
+   
+   # OR using any other static file server
+   ```
+
+3. **Update API Endpoint**:
+   Modify `code.js` line 23 to use your local lambda function:
+   ```javascript
+   const url = `http://localhost:9000/.netlify/functions/get?id=${currentID}&q=${qualitySelect.value}`;
+   ```
+
+4. **Access the Application**:
+   Open your browser and navigate to `http://localhost:8080`
+
+#### Option 2: Quick Preview (Frontend Only)
+
+Simply open `index.html` in your web browser. Note that this will use the deployed Lambda function and may not work if the deployment is unavailable.
+
+### Production Build
+
+To prepare the Lambda functions for deployment:
+
+```bash
+npm run build:lambda
+```
+
+This creates optimized Lambda function bundles in the `lambda/` directory.
+
+## 🎯 Usage
+
+### Basic Usage
+
+1. **Open the Application**: Navigate to the web interface in your browser
+2. **Enter Vimeo URL**: Paste a Vimeo video URL in the input field
+
+   **Supported URL Formats**:
+   ```
+   https://vimeo.com/123456789
+   https://vimeo.com/channels/staffpicks/123456789
+   https://vimeo.com/ondemand/moviename/123456789
+   vimeo.com/123456789
+   ```
+
+3. **Select Quality**: Choose your preferred video quality from the dropdown:
+   - **1080P** (1920px width)
+   - **720P** (1280px width) 
+   - **540P** (960px width)
+   - **360P** (640px width)
+
+4. **Process Video**: Click the play button or press Enter
+
+5. **Download**: Two links will appear:
+   - **(video link)** - Click to view or right-click → "Save as" to download
+   - **(poster link)** - Direct link to the video thumbnail
+
+### Example Workflow
+
+```
+Input: https://vimeo.com/channels/staffpicks/212731897
+Quality: 720P
+Output: 
+  ✅ (video link) - Direct MP4 download
+  ✅ (poster link) - Thumbnail image
+```
+
+## 📁 Project Structure
+
+```
+vimeo-ripper/
+├── 📄 index.html          # Main web interface
+├── 🎨 style.css           # Application styling
+├── ⚙️ code.js             # Frontend logic and API calls
+├── 📦 package.json        # Dependencies and scripts
+├── 🚀 netlify.toml        # Netlify deployment configuration
+├── 🖼️ img/               # UI assets
+│   └── play-button.svg
+├── 🔧 src/
+│   └── lambda/
+│       └── get.js         # Serverless function for video processing
+└── 🏗️ lambda/            # Built Lambda functions (generated)
+```
+
+### Key Files Explained
+
+- **`index.html`**: The main user interface with input validation and result display
+- **`code.js`**: Handles URL validation, API calls, and user interactions
+- **`style.css`**: Responsive design with gradient background and modern styling
+- **`src/lambda/get.js`**: Serverless function that fetches video data from Vimeo's API
+- **`netlify.toml`**: Configuration for Netlify deployment
+
+## 🔧 API Documentation
+
+### Lambda Function Endpoint
+
+**URL**: `/.netlify/functions/get`
+
+**Method**: `GET`
+
+**Parameters**:
+- `id` (required): Vimeo video ID extracted from the URL
+- `q` (optional): Requested video quality width (640, 960, 1280, 1920)
+
+**Response Format**:
+```json
+{
+  "video": "https://direct-video-url.mp4",
+  "poster": "https://thumbnail-image-url.jpg"
+}
+```
+
+**Error Response**:
+```json
+{
+  "error": "The server encountered a problem when trying to get the video.",
+  "details": "Error details here"
+}
+```
+
+### Frontend API Usage
+
+The frontend uses the Fetch API to communicate with the Lambda function:
+
+```javascript
+const url = `https://vget.netlify.com/.netlify/functions/get?id=${videoID}&q=${quality}`;
+fetch(url)
+  .then(response => response.json())
+  .then(data => {
+    // Handle video and poster URLs
+  });
+```
+
+## ⚙️ Configuration
+
+### Environment Variables
+
+For production deployment, you may want to configure:
+
+- **API_BASE_URL**: Base URL for your deployed Lambda functions
+- **CORS_ORIGIN**: Allowed origins for CORS (currently set to "*")
+
+### URL Validation Regex
+
+The application uses a comprehensive regex pattern to validate Vimeo URLs:
+
+```javascript
+const regex = new RegExp(`^((https?):\/)?\/?(vimeo.com)((\/\\w+)*\/)([0-9]+[^#?\s]+)(.*)?(#[\\w\\-]+)?$`);
+```
+
+This pattern supports various Vimeo URL formats including channels, on-demand content, and direct video links.
+
+## 🚀 Deployment
+
+### Netlify Deployment (Recommended)
+
+1. **Connect Repository**: Link your GitHub repository to Netlify
+
+2. **Configure Build Settings**:
+   - Build command: `npm run build:lambda`
+   - Publish directory: `./` (root directory)
+   - Functions directory: `lambda`
+
+3. **Deploy**: Netlify will automatically build and deploy your application
+
+### Manual Deployment
+
+1. **Build Lambda Functions**:
+   ```bash
+   npm run build:lambda
+   ```
+
+2. **Upload Files**: Upload all files including the generated `lambda/` directory to your hosting provider
+
+3. **Configure Serverless Functions**: Ensure your hosting provider supports serverless functions compatible with Netlify Lambda
+
+## 🐛 Troubleshooting
+
+### Common Issues
+
+**1. "Failed" Links Appear**
+- **Cause**: Video may be private, embedded disabled, or region-restricted
+- **Solution**: Try a different Vimeo video or check video privacy settings
+
+**2. Lambda Function Not Working Locally**
+- **Cause**: Netlify Lambda dev server not running
+- **Solution**: Ensure `npm run start:lambda` is running and update API endpoint in `code.js`
+
+**3. CORS Errors**
+- **Cause**: Browser blocking cross-origin requests
+- **Solution**: Serve the frontend through a proper HTTP server, not file:// protocol
+
+**4. Invalid URL Error**
+- **Cause**: URL doesn't match expected Vimeo format
+- **Solution**: Ensure URL contains a valid Vimeo video ID (numbers only)
+
+### Debug Mode
+
+To enable debugging, open browser developer tools and monitor:
+- Network tab for API requests
+- Console tab for JavaScript errors
+- Application tab for any storage issues
+
+## 🤝 Contributing
+
+Contributions are welcome! Here's how to get started:
+
+1. **Fork the Repository**
+2. **Create a Feature Branch**: `git checkout -b feature/amazing-feature`
+3. **Make Changes**: Implement your feature or fix
+4. **Test Thoroughly**: Ensure all functionality works
+5. **Commit Changes**: `git commit -m 'Add amazing feature'`
+6. **Push to Branch**: `git push origin feature/amazing-feature`
+7. **Open Pull Request**: Submit your changes for review
+
+### Development Guidelines
+
+- Follow existing code style and conventions
+- Test with multiple Vimeo video types
+- Ensure responsive design compatibility
+- Update documentation for new features
+
+## 📄 License
+
+This project is licensed under the ISC License. See the `package.json` file for details.
+
+## 🙏 Acknowledgments
+
+- **Vimeo**: For providing the player configuration API
+- **Netlify**: For serverless function hosting and deployment platform
+- **Open Source Community**: For the various libraries and tools used
+
+## 📞 Support
+
+If you encounter issues or have questions:
+
+1. Check the [Issues](https://github.com/6a/vimeo-ripper/issues) page
+2. Create a new issue with detailed information
+3. Include browser version, error messages, and steps to reproduce
+
+---
+
+**⚠️ Disclaimer**: This tool is for educational purposes. Please respect content creators' rights and Vimeo's Terms of Service. Only download videos you have permission to access.

BIN
android-chrome-192x192.png


BIN
android-chrome-256x256.png


BIN
apple-touch-icon.png


+ 9 - 0
browserconfig.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+    <msapplication>
+        <tile>
+            <square150x150logo src="/mstile-150x150.png"/>
+            <TileColor>#2b5797</TileColor>
+        </tile>
+    </msapplication>
+</browserconfig>

+ 89 - 0
code.js

@@ -0,0 +1,89 @@
+// ^((https?):\/)?\/?(vimeo.com)((\/\\w+)*\/)([0-9]+[^#?\s]+)(.*)?(#[\\w\\-]+)?$
+// https://stackoverflow.com/questions/27745/getting-parts-of-a-url-regex
+
+const regex = new RegExp(`^((https?):\/)?\/?(vimeo.com)((\/\\w+)*\/)([0-9]+[^#?\s]+)(.*)?(#[\\w\\-]+)?$`);
+const inputWrapper = document.getElementById("input-wrapper");
+const inputField = document.getElementById("input-field");
+const button = document.getElementById("button");
+const qualitySelect = document.getElementById("selector");
+const tipText = document.getElementById("tip");
+
+var videoUrlText = document.getElementById("result-url-txt");
+var posterUrlText = document.getElementById("result-poster-txt");
+var videoUrlAnchor = document.getElementById("result-url-anchor");
+var posterUrlAnchor = document.getElementById("result-poster-anchor");
+var currentID = -1;
+
+function hideLinks() {
+    tipText.classList.add("inactive");
+    videoUrlAnchor.classList.add("inactive");
+    posterUrlAnchor.classList.add("inactive");
+}
+
+function get() {
+    if (currentID != -1) {
+        hideLinks();
+        const url = `https://vget.netlify.com/.netlify/functions/get?id=${currentID}&q=${qualitySelect.value}`;
+        fetch(url)
+        .then(data=>{
+            return data.json();
+        })
+        .then(res=>{
+            if (!res.video) {
+                videoUrlAnchor.removeAttribute("href");
+                videoUrlText.innerText = "(failed)";
+                posterUrlAnchor.removeAttribute("href");
+                posterUrlText.innerText = "(failed)";
+            } else {
+                videoUrlAnchor.href = (res.video);
+                videoUrlText.innerText = "(video link)";
+                posterUrlAnchor.href = (res.poster);
+                posterUrlText.innerText = "(poster link)";
+            }
+
+            tipText.classList.remove("inactive");
+            videoUrlAnchor.classList.remove("inactive");
+            posterUrlAnchor.classList.remove("inactive");
+        })
+        .catch(error=>{
+            videoUrlAnchor.removeAttribute("href");
+            videoUrlText.innerText = "(failed)";
+            posterUrlAnchor.removeAttribute("href");
+            posterUrlText.innerText = "(failed)";
+        });
+    }
+}
+
+inputField.addEventListener("input", function (e) {
+    var rr = regex.exec(inputField.value);
+
+    if (rr && rr.length >= 7 && !isNaN(rr[6])) {
+        inputWrapper.classList.add("regex-good");
+        inputWrapper.classList.remove("regex-bad");
+
+        button.disabled  = false;
+        button.classList.add("fill-good");
+        button.classList.remove("fill-bad");
+        currentID = rr[6];
+    } else {
+        inputWrapper.classList.add("regex-bad");
+        inputWrapper.classList.remove("regex-good");
+
+        button.disabled  = true;
+        button.classList.add("fill-bad");
+        button.classList.remove("fill-good");
+        currentID = -1;
+    }
+});
+
+inputField.addEventListener("keypress", function (e) {
+    if (e.keyCode == 13) {
+        get();
+    }
+});
+
+
+button.addEventListener("click", function(e) {
+    get();
+});
+

BIN
favicon-16x16.png


BIN
favicon-32x32.png



+ 35 - 0
img/play-button.svg

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 41.999 41.999" style="enable-background:new 0 0 41.999 41.999;" xml:space="preserve" width="512px" height="512px">
+<path d="M36.068,20.176l-29-20C6.761-0.035,6.363-0.057,6.035,0.114C5.706,0.287,5.5,0.627,5.5,0.999v40  c0,0.372,0.206,0.713,0.535,0.886c0.146,0.076,0.306,0.114,0.465,0.114c0.199,0,0.397-0.06,0.568-0.177l29-20  c0.271-0.187,0.432-0.494,0.432-0.823S36.338,20.363,36.068,20.176z" fill="#f2f2f2"/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 51 - 0
index.html

@@ -0,0 +1,51 @@
+<!doctype html>
+
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+
+    <title>vget: Vimeo video ripper</title>
+    <meta name="description" content="vget">
+    <meta name="author" content="6a">
+
+    <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png">
+    <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png">
+    <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png">
+    <link rel="manifest" href="./site.webmanifest">
+    <link rel="mask-icon" href="./safari-pinned-tab.svg" color="#5bbad5">
+    <meta name="msapplication-TileColor" content="#2b5797">
+    <meta name="theme-color" content="#ffffff">
+
+    <link rel="stylesheet" href="style.css">
+
+</head>
+
+<body>
+    <div class="app">
+        <p class="noselect">enter the full video path</p>
+        <h4>example: https://vimeo.com/channels/staffpicks/212731897</h4>
+        <div id="input-wrapper" class="input-section">
+            <input id="input-field" type="text" class="" spellcheck="false">
+            <select id="selector">
+                <option value="1920">1080P</option>
+                <option value="1280">720P</option>
+                <option value="960">540P</option>
+                <option value="640">360P</option>
+            </select>
+            <button id="button" disabled><img class="go-button" src="./img/play-button.svg" alt=""></button>
+        </div>
+        <div class="link-box">
+            <a target="_blank" href="" class="inactive" id="result-url-anchor">
+                <h5 id="result-url-txt">www</h5>
+            </a>
+            <a target="_blank" href="" class="inactive" id="result-poster-anchor">
+                <h5 id="result-poster-txt">www</h5>
+            </a>
+        </div>
+        <h4 id="tip" class="inactive">tip: click on the links to view them, or right click -> save as to download.</h4>
+    </div>
+    <script src="code.js"></script>
+</body>
+
+</html>

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
lambda/get.js


BIN
mstile-150x150.png


+ 2 - 0
netlify.toml

@@ -0,0 +1,2 @@
+[build]
+  Functions = "lambda"

+ 27 - 0
package.json

@@ -0,0 +1,27 @@
+{
+  "name": "vimeo-ripper-test",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start:lambda": "netlify-lambda serve src/lambda",
+    "build:lambda": "netlify-lambda build src/lambda"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/6a/vimeo-ripper.git"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "bugs": {
+    "url": "https://github.com/6a/vimeo-ripper/issues"
+  },
+  "homepage": "https://github.com/6a/vimeo-ripper#readme",
+  "dependencies": {
+    "axios": "^0.18.0",
+    "netlify-lambda": "^1.0.3",
+    "node-fetch": "^2.3.0"
+  }
+}

+ 1 - 0
safari-pinned-tab.svg

@@ -0,0 +1 @@
+<svg version="1" xmlns="http://www.w3.org/2000/svg" width="346.667" height="346.667" viewBox="0 0 260.000000 260.000000"><path d="M52.5 1.6C27 7 6.6 27.7 1.5 53.5.1 60.5 0 70.9.2 134c.4 81.3-.1 75.6 8.1 91.4 6 11.6 15.8 21.1 27.9 27.1 15.8 7.7 14.1 7.6 97.2 7.3l73.1-.3 8-2.8c20.5-7.3 34.9-21.7 42.2-42.2l2.8-8 .3-73.1c.3-83.1.4-81.4-7.3-97.2-6-12.1-15.5-21.9-27.1-27.9C209.5.1 215.5.6 133 .3 71.2.2 58.4.4 52.5 1.6zm33.6 32.6c0 .2 4.6 29.9 10.2 66.1l10.2 65.8 39.9-66.1 40-66h22.8c12.5 0 22.8.2 22.8.4 0 .3-26.7 43.1-59.4 95.3l-59.3 94.8-20 .3-19.9.2-14.7-94.2C50.6 78.9 44 35.9 44 35.2c0-.9 4.9-1.2 21-1.2 11.6 0 21 .1 21.1.2z"/></svg>

+ 19 - 0
site.webmanifest

@@ -0,0 +1,19 @@
+{
+    "name": "vget",
+    "short_name": "vget",
+    "icons": [
+        {
+            "src": "/android-chrome-192x192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "/android-chrome-256x256.png",
+            "sizes": "256x256",
+            "type": "image/png"
+        }
+    ],
+    "theme_color": "#ffffff",
+    "background_color": "#ffffff",
+    "display": "standalone"
+}

+ 49 - 0
src/lambda/get.js

@@ -0,0 +1,49 @@
+const axios = require('axios');
+
+exports.handler = (event, context, callback) => {
+  const id = event.queryStringParameters.id;
+  const url = `https://player.vimeo.com/video/${id}/config`;
+  const quality = parseInt(event.queryStringParameters.q);
+  axios.get(url)
+    .then((res) => {
+      var bestRes = 0;
+      var bestWidth = 0;
+      var videoUrl = "";
+      if (quality && [640, 960, 1280, 1920].includes(quality)) {
+        for (var index = 0; index < res.data.request.files.progressive.length; index++) {
+          if (res.data.request.files.progressive[index].width == quality) {
+            videoUrl = res.data.request.files.progressive[index].url;
+            bestRes = res.data.request.files.progressive[index].height;
+            bestWidth = res.data.request.files.progressive[index].width;
+            break;
+          }
+        } 
+      }
+
+      if (!quality || videoUrl == "") {
+        for (var index = 0; index < res.data.request.files.progressive.length; index++) {
+          if (res.data.request.files.progressive[index].height > bestRes) {
+            videoUrl = res.data.request.files.progressive[index].url;
+            bestRes = res.data.request.files.progressive[index].height;
+            bestWidth = res.data.request.files.progressive[index].width;
+          }
+        }
+      }
+
+      var posterURL = res.data.video.thumbs[quality] || res.data.video.thumbs[bestWidth] || res.data.video.thumbs[1920]
+      || res.data.video.thumbs[1280] || res.data.video.thumbs[960] || res.data.video.thumbs[640];
+
+      callback(null, {
+        statusCode: 200,
+        body: `{"video": "${videoUrl}", "poster": "${posterURL}"}`,
+        headers: { "Access-Control-Allow-Origin": "*" }
+      });
+    })
+    .catch((err) => {
+      callback(null, {
+        statusCode: 500,
+        body: `{"error": "The server encountered a problem when trying to get the video.", "details": "${err}"}`,
+        headers: { "Access-Control-Allow-Origin": "*" }
+      });
+    });
+};

+ 189 - 0
style.css

@@ -0,0 +1,189 @@
+html {
+    width: 100%;
+    height: 100%;
+    font-family: Consolas, 'Courier New', 'Lucida Console', monospace;
+}
+
+body {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    padding: 0;
+    background-color: #af2184;
+    background-image: -webkit-repeating-linear-gradient(top left, #e0620d 0%, #af2184 100%);
+    background-image: repeating-linear-gradient(to bottom right, #e0620d 0%, #af2184 100%);
+    background-image: -ms-repeating-linear-gradient(top left, #e0620d 0%, #af2184 100%);
+}
+
+.noselect {
+    -webkit-touch-callout: none; /* iOS Safari */
+      -webkit-user-select: none; /* Safari */
+       -khtml-user-select: none; /* Konqueror HTML */
+         -moz-user-select: none; /* Firefox */
+          -ms-user-select: none; /* Internet Explorer/Edge */
+              user-select: none; /* Non-prefixed version, currently
+                                    supported by Chrome and Opera */
+}
+
+.app {
+    width: 100%;
+    max-width: 900px;
+    height: 100%;
+    max-height: 600px;
+    margin: 20px;
+    background-color: rgb(255, 255, 255);
+    box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+    position: relative;
+}
+
+.app input {
+    background-color: rgba(255, 255, 255, 0);
+    text-align: left;
+    outline: none;
+    border: none;
+    line-height: 2em;
+    font-family: Consolas, 'Courier New', 'Lucida Console', monospace;
+    flex: 1;
+    margin: 0 20px;
+
+}
+
+.input-section {
+    width: 60%;
+    text-align: center;
+    border-radius: 1000px;
+    border: 2px solid rgba(97, 223, 154, 0);
+    background-color: rgb(236, 236, 236);
+    height: 40px;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+
+    position: relative;
+}
+
+
+.input-section:hover {
+    background-color: rgb(231, 231, 231);
+}
+
+.app select {
+    outline: none;
+    padding: 2px;
+    font-family: Consolas, 'Courier New', 'Lucida Console', monospace;
+    border: 1px solid rgb(169, 169, 169);
+    height: 28px;
+    margin: 6px 0px;
+    background-color: rgb(255, 255, 255);
+}
+
+.app select:hover {
+    outline: none;
+}
+
+.app button {
+    border: none;
+    outline: none;
+    margin: 4px;
+    height: 32px;
+    width: 32px;
+    border-radius: 1000px;
+    color: rgba(182, 182, 182, 0.8);
+    font-family: Consolas, 'Courier New', 'Lucida Console', monospace;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.go-button {
+    height: 18px;
+    width: 18px;
+    margin-left: 3px;
+}
+
+.app button:disabled {
+    background-color: rgb(255, 255, 255);
+    cursor: default;
+}
+
+.app button:enabled {
+    cursor: pointer !important;
+}
+
+.app button:enabled:hover {
+    background-color: rgb(69, 211, 133) !important;
+}
+
+.app p {
+    font-weight: bold;
+    color: rgba(0, 0, 0, 0.8);
+}
+
+.app h4 {
+    font-weight: unset;
+    font-style: italic;
+    margin: 5px 0 20px 0;
+    font-size: 0.8em;
+    color: rgba(0, 0, 0, 0.7);
+    visibility: visible;
+    opacity: 1;
+    transition: all 0.4s ease-in-out;
+}
+
+.app h5 {
+    margin: 5px 0;
+    font-weight: bold;
+    font-size: 1.05em;
+    color: rgba(81, 53, 97, 0.8);
+}
+
+.app a {
+    cursor: pointer;
+    text-decoration: none;
+    visibility: visible;
+    opacity: 1;
+    transition: all 0.4s ease-in-out;
+}
+
+.link-box {
+    margin-top: 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    width: 40%;
+}
+
+#tip {
+    position: absolute;
+    bottom: 0;
+}
+
+.regex-good {
+    border: 2px solid rgb(97, 223, 153) !important;
+}
+
+.regex-bad {
+    border: 2px solid rgb(207, 76, 76) !important;
+}
+
+.fill-good {
+    background-color: rgb(97, 223, 153) !important;
+    color: white !important;
+}
+
+.fill-bad {
+    background-color: rgb(207, 76, 76) !important;
+    color: white !important;
+}
+
+.inactive {
+    visibility: hidden !important;
+    opacity: 0 !important;
+}

Деякі файли не було показано, через те що забагато файлів було змінено