Merge pull request #533 from arabcoders/dev
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled
Refactor: add enabled field to conditions
This commit is contained in:
commit
a924ddb718
16 changed files with 512 additions and 877 deletions
11
API.md
11
API.md
|
|
@ -1510,7 +1510,8 @@ or an error:
|
|||
"name": "condition_name",
|
||||
"filter": "...",
|
||||
"cli": "...",
|
||||
...
|
||||
"extras": {},
|
||||
"enabled": true
|
||||
},
|
||||
...
|
||||
]
|
||||
|
|
@ -1529,6 +1530,8 @@ or an error:
|
|||
"name": "Use proxy for region locked content",
|
||||
"filter": "availability = 'needs_auth' & channel_id = 'channel_id'",
|
||||
"cli": "--proxy http://myproxy.com:8080",
|
||||
"extras": {},
|
||||
"enabled": true
|
||||
},
|
||||
...
|
||||
]
|
||||
|
|
@ -1542,11 +1545,17 @@ or an error:
|
|||
"name": "Use proxy for region locked content",
|
||||
"filter": "availability = 'needs_auth' & channel_id = 'channel_id'",
|
||||
"cli": "--proxy http://myproxy.com:8080",
|
||||
"extras": {},
|
||||
"enabled": true
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
- Disabled conditions (`enabled: false`) will be stored but ignored during matching.
|
||||
- All conditions are enabled by default when the `enabled` field is not provided.
|
||||
|
||||
---
|
||||
|
||||
### POST /api/conditions/test
|
||||
|
|
|
|||
764
CONTRIBUTING.md
764
CONTRIBUTING.md
|
|
@ -1,715 +1,149 @@
|
|||
# Contributing to YTPTube
|
||||
|
||||
Thank you for your interest in contributing to **YTPTube**! We welcome contributions from the community to help improve this project.
|
||||
YTPTube is a **personal-first project**. While contributions are welcome, **all final decisions rest with the maintainer**.
|
||||
|
||||
This document provides guidelines and instructions for contributing to the project.
|
||||
## Core Principles
|
||||
|
||||
## Table of Contents
|
||||
* **The maintainer has final say** on all changes and project direction.
|
||||
* This project is built **for personal use first**. Community contributions are secondary.
|
||||
* **All contributions require prior discussion and approval.**
|
||||
* **All pull requests must target the `dev` branch** never `master`.
|
||||
* Contributions must align with the project's goals, architecture, and coding standards.
|
||||
|
||||
- [Contributing to YTPTube](#contributing-to-ytptube)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Backend Requirements](#backend-requirements)
|
||||
- [Frontend Requirements](#frontend-requirements)
|
||||
- [Optional Tools](#optional-tools)
|
||||
- [Installing Required Package Managers](#installing-required-package-managers)
|
||||
- [Installing uv (Python Package Manager)](#installing-uv-python-package-manager)
|
||||
- [Installing pnpm (Node.js Package Manager)](#installing-pnpm-nodejs-package-manager)
|
||||
- [Cloning the Repository](#cloning-the-repository)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Backend Setup (Python)](#backend-setup-python)
|
||||
- [Frontend Setup (Node.js)](#frontend-setup-nodejs)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Working on a Feature or Bug Fix](#working-on-a-feature-or-bug-fix)
|
||||
- [Branching Strategy](#branching-strategy)
|
||||
- [Important Rules:](#important-rules)
|
||||
- [Branch Naming Conventions:](#branch-naming-conventions)
|
||||
- [Testing Requirements](#testing-requirements)
|
||||
- [Backend Testing (Python)](#backend-testing-python)
|
||||
- [Frontend Testing (TypeScript/Vue)](#frontend-testing-typescriptvue)
|
||||
- [Test Coverage Expectations](#test-coverage-expectations)
|
||||
- [Code Quality and Linting](#code-quality-and-linting)
|
||||
- [Backend (Python)](#backend-python)
|
||||
- [Frontend (TypeScript/Vue)](#frontend-typescriptvue)
|
||||
- [Code Quality Checklist](#code-quality-checklist)
|
||||
- [Pre-commit Hooks](#pre-commit-hooks)
|
||||
- [Setting Up Pre-commit Hooks](#setting-up-pre-commit-hooks)
|
||||
- [Before Submitting](#before-submitting)
|
||||
- [Pull Request Guidelines](#pull-request-guidelines)
|
||||
- [Creating a Pull Request](#creating-a-pull-request)
|
||||
- [PR Review Process](#pr-review-process)
|
||||
- [After Your PR is Merged](#after-your-pr-is-merged)
|
||||
- [Code Style](#code-style)
|
||||
- [Python Code Style](#python-code-style)
|
||||
- [TypeScript/Vue Code Style](#typescriptvue-code-style)
|
||||
- [Getting Help](#getting-help)
|
||||
- [Resources](#resources)
|
||||
- [Ask Questions](#ask-questions)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [License](#license)
|
||||
**Opening a PR without prior approval will result in immediate closure without review.**
|
||||
|
||||
---
|
||||
|
||||
## Code of Conduct
|
||||
## Contribution Process
|
||||
|
||||
Please be respectful and constructive in all interactions. We aim to maintain a welcoming and inclusive community.
|
||||
### 1. Start a Discussion
|
||||
|
||||
---
|
||||
Before writing any code, **propose your idea** through one of these channels:
|
||||
|
||||
## Getting Started
|
||||
* Open a **GitHub Issue** for bug fixes or small features
|
||||
* Start a **GitHub Discussion** for larger changes or questions
|
||||
|
||||
### Prerequisites
|
||||
**Include in your proposal:**
|
||||
* **What** you want to change
|
||||
* **Why** it's needed or beneficial
|
||||
* Bug fixes and functional improvements are prioritized
|
||||
* Performance enhancements with measurable impact
|
||||
* Features that align with the project's core purpose
|
||||
* **Not acceptable:** Personal UI/UX preferences, stylistic changes, or "I think it looks better" rationale
|
||||
* **How** you plan to implement it (high-level approach)
|
||||
* Any relevant context or use cases
|
||||
|
||||
Before you begin, ensure you have the following tools installed on your system:
|
||||
### 2. Wait for Approval
|
||||
|
||||
#### Backend Requirements
|
||||
* Only proceed after **explicit approval** from the maintainer to not waste effort.
|
||||
* The maintainer may suggest modifications or alternative approaches.
|
||||
* Not all proposals will be accepted this protects project coherence.
|
||||
|
||||
- **Python 3.13+**: YTPTube requires Python 3.13 or higher
|
||||
- **uv**: Modern Python package manager and virtual environment tool
|
||||
|
||||
#### Frontend Requirements
|
||||
|
||||
- **Node.js 18+**: Required for the Nuxt/Vue frontend
|
||||
- **pnpm**: Fast, disk space efficient package manager
|
||||
|
||||
#### Optional Tools
|
||||
|
||||
- **Git**: Version control
|
||||
- **Docker**: For containerized development (optional)
|
||||
- **ffmpeg**: Required for video player functionality
|
||||
|
||||
### Installing Required Package Managers
|
||||
|
||||
#### Installing uv (Python Package Manager)
|
||||
|
||||
**uv** is a fast Python package installer and resolver written in Rust. It's used for managing Python dependencies and virtual environments.
|
||||
|
||||
**On Linux/macOS:**
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
**On Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
```
|
||||
|
||||
**Using pip:**
|
||||
|
||||
```bash
|
||||
pip install uv
|
||||
```
|
||||
|
||||
**Verify installation:**
|
||||
|
||||
```bash
|
||||
uv --version
|
||||
```
|
||||
|
||||
For more information, visit: https://github.com/astral-sh/uv
|
||||
|
||||
#### Installing pnpm (Node.js Package Manager)
|
||||
|
||||
**pnpm** is a fast, disk space efficient package manager for Node.js.
|
||||
|
||||
**Using npm:**
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
**Using Homebrew (macOS):**
|
||||
|
||||
```bash
|
||||
brew install pnpm
|
||||
```
|
||||
|
||||
**Using Scoop (Windows):**
|
||||
|
||||
```bash
|
||||
scoop install pnpm
|
||||
```
|
||||
|
||||
**Using standalone script:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.pnpm.io/install.sh | sh -
|
||||
```
|
||||
|
||||
**Verify installation:**
|
||||
|
||||
```bash
|
||||
pnpm --version
|
||||
```
|
||||
|
||||
For more information, visit: https://pnpm.io/installation
|
||||
|
||||
### Cloning the Repository
|
||||
|
||||
Clone the repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/arabcoders/ytptube.git
|
||||
cd ytptube
|
||||
```
|
||||
|
||||
**Important**: Always work on the `dev` branch, not `master`/`main`:
|
||||
|
||||
```bash
|
||||
git checkout dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Backend Setup (Python)
|
||||
|
||||
1. **Navigate to the project root:**
|
||||
|
||||
```bash
|
||||
cd ytptube
|
||||
```
|
||||
|
||||
2. **Install Python dependencies using uv:**
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
This will:
|
||||
- Create a virtual environment (`.venv/`)
|
||||
- Install all dependencies from `pyproject.toml`
|
||||
- Install development dependencies
|
||||
|
||||
3. **Install development dependencies:**
|
||||
|
||||
Development dependencies are defined in `pyproject.toml` under `[dependency-groups]`:
|
||||
|
||||
```bash
|
||||
uv sync --group dev
|
||||
```
|
||||
|
||||
4. **Verify installation:**
|
||||
|
||||
```bash
|
||||
uv run python --version
|
||||
uv run pytest --version
|
||||
```
|
||||
|
||||
5. **Run the backend development server:**
|
||||
|
||||
```bash
|
||||
uv run app/main.py
|
||||
```
|
||||
|
||||
The backend API will be available at `http://localhost:8081`
|
||||
|
||||
### Frontend Setup (Node.js)
|
||||
|
||||
1. **Navigate to the UI directory:**
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
```
|
||||
|
||||
2. **Install Node.js dependencies using pnpm:**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Create a `.env` file in the `ui/` directory based on `.env.example` to configure environment variables.
|
||||
> to link the frontend to the backend API.
|
||||
|
||||
3. **Run the frontend development server:**
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The frontend will be available at `http://localhost:8082` (or the port shown in the terminal)
|
||||
|
||||
4. **Verify the setup:**
|
||||
|
||||
```bash
|
||||
pnpm run typecheck
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Working on a Feature or Bug Fix
|
||||
|
||||
1. **Ensure you're on the `dev` branch:**
|
||||
### 3. Develop Your Changes
|
||||
|
||||
**Branch from `dev`:**
|
||||
```bash
|
||||
git checkout dev
|
||||
git pull origin dev
|
||||
git checkout -b feature/descriptive-name
|
||||
```
|
||||
|
||||
2. **Create a feature branch:**
|
||||
**Follow project standards:**
|
||||
* Match existing code style and conventions
|
||||
* Add or update tests for all changes:
|
||||
* New features **MUST** include tests
|
||||
* Bug fixes **MUST** include a regression test
|
||||
* Ensure all linting and tests pass
|
||||
* Keep changes focused and atomic
|
||||
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
# or
|
||||
git checkout -b fix/bug-description
|
||||
```
|
||||
### 4. Submit a Pull Request
|
||||
|
||||
3. **Make your changes**
|
||||
**Target the `dev` branch:**
|
||||
* Reference the approved issue or discussion number
|
||||
* Provide a clear description of what changed and why
|
||||
* List any breaking changes or migration steps
|
||||
* Ensure CI checks pass
|
||||
|
||||
4. **Test your changes** (see [Testing Requirements](#testing-requirements))
|
||||
|
||||
5. **Run code quality checks** (see [Code Quality and Linting](#code-quality-and-linting))
|
||||
|
||||
6. **Commit your changes:**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: add new feature description"
|
||||
# or
|
||||
git commit -m "fix: fix bug description"
|
||||
```
|
||||
|
||||
7. **Push to your fork:**
|
||||
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
8. **Create a Pull Request** targeting the `dev` branch
|
||||
**PR template checklist:**
|
||||
- [ ] Discussed and approved beforehand
|
||||
- [ ] Targets `dev` branch
|
||||
- [ ] Tests added/updated and passing
|
||||
- [ ] Linting passes
|
||||
- [ ] Documentation updated (if needed)
|
||||
|
||||
---
|
||||
|
||||
## Branching Strategy
|
||||
## Automatic Rejections
|
||||
|
||||
**YTPTube uses a two-branch strategy:**
|
||||
The following will be **closed immediately without review**:
|
||||
|
||||
- **`master`**: Production-ready code. Only maintainers merge to this branch.
|
||||
- **`dev`**: Development branch. **All contributions must target this branch.**
|
||||
* PRs opened without prior discussion and approval
|
||||
* PRs targeting `master` instead of `dev`
|
||||
* Large refactors or architectural changes without approval
|
||||
* Fully AI-generated code without meaningful human oversight
|
||||
* Changes that don't align with project goals or philosophy
|
||||
* PRs where the contributor cannot explain or justify the changes
|
||||
|
||||
### Important Rules:
|
||||
---
|
||||
|
||||
- ✅ **DO**: Create feature branches from `dev`
|
||||
- ✅ **DO**: Submit pull requests to `dev`
|
||||
- ❌ **DON'T**: Submit pull requests to `master`/`main`
|
||||
- ❌ **DON'T**: Commit directly to `dev` (use feature branches)
|
||||
## AI-Assisted Development
|
||||
|
||||
### Branch Naming Conventions:
|
||||
AI tools are **permitted** as development aids, but **you remain fully responsible** for all submitted code.
|
||||
|
||||
- `feature/description` - For new features
|
||||
- `fix/description` - For bug fixes
|
||||
- `refactor/description` - For code refactoring
|
||||
- `docs/description` - For documentation updates
|
||||
- `test/description` - For test additions/improvements
|
||||
### Acceptable Use
|
||||
|
||||
AI-assisted code is welcome **when**:
|
||||
|
||||
* You **fully understand** every line being submitted
|
||||
* The code **seamlessly integrates** with existing patterns and style
|
||||
* You have **reviewed, tested, and validated** the output yourself
|
||||
* **Comprehensive tests** are included (not AI-generated stubs)
|
||||
* The code is **indistinguishable in quality** from hand-written contributions
|
||||
* You can **explain and defend** design decisions in the PR
|
||||
|
||||
**AI is a tool, not a substitute for understanding.**
|
||||
|
||||
### Not Acceptable
|
||||
|
||||
The following will be rejected:
|
||||
|
||||
* Fully AI-generated PRs with minimal human review
|
||||
* Code that introduces new patterns or abstractions without approval
|
||||
* "Prompt-dump" output that doesn't match project conventions
|
||||
* Changes the contributor cannot explain or justify
|
||||
* AI-generated test suites that don't meaningfully validate behavior
|
||||
|
||||
### Disclosure
|
||||
|
||||
You are **not required** to disclose AI usage. However, if code quality suggests pure AI generation without human oversight, the PR will be closed.
|
||||
|
||||
**You are accountable for correctness, maintainability, and alignment regardless of how the code was created.**
|
||||
|
||||
---
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
**All code changes must include appropriate tests.** This ensures code quality and prevents regressions.
|
||||
All contributions must include appropriate tests:
|
||||
|
||||
### Backend Testing (Python)
|
||||
* **New features:** Full test coverage including edge cases
|
||||
* **Bug fixes:** Regression test that fails before the fix and passes after
|
||||
* **Refactors:** Existing tests must continue to pass
|
||||
* **Performance changes:** Benchmarks or performance tests when applicable
|
||||
|
||||
Tests are located in `app/tests/` and use **pytest**.
|
||||
|
||||
**Running tests:**
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
uv run pytest
|
||||
|
||||
# Run specific test file
|
||||
uv run pytest app/tests/test_download.py
|
||||
|
||||
# Run with verbose output
|
||||
uv run pytest -v
|
||||
|
||||
# Run with coverage
|
||||
uv run pytest --cov=app --cov-report=html
|
||||
```
|
||||
|
||||
**Creating new tests:**
|
||||
|
||||
1. Create a test file in `app/tests/` with the prefix `test_`
|
||||
2. Use pytest conventions:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from app.library.YourModule import YourClass
|
||||
|
||||
class TestYourClass:
|
||||
def test_feature_name(self):
|
||||
"""Test description."""
|
||||
# Arrange
|
||||
instance = YourClass()
|
||||
|
||||
# Act
|
||||
result = instance.method()
|
||||
|
||||
# Assert
|
||||
assert result == expected_value
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_feature(self):
|
||||
"""Test async functionality."""
|
||||
result = await async_function()
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
**Testing Singleton Classes:**
|
||||
|
||||
When testing singleton classes, always reset the singleton instance:
|
||||
|
||||
```python
|
||||
from app.library.Presets import Presets
|
||||
|
||||
class TestPresets:
|
||||
def setup_method(self):
|
||||
"""Reset singleton before each test."""
|
||||
Presets._reset_singleton()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Reset singleton after each test."""
|
||||
Presets._reset_singleton()
|
||||
```
|
||||
|
||||
### Frontend Testing (TypeScript/Vue)
|
||||
|
||||
Tests are located in `ui/tests/` and use **Vitest**.
|
||||
|
||||
**Running tests:**
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run in watch mode
|
||||
pnpm test:watch
|
||||
|
||||
# Run with coverage
|
||||
pnpm run test --coverage
|
||||
```
|
||||
|
||||
**Creating new tests:**
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import YourComponent from '~/components/YourComponent.vue'
|
||||
|
||||
describe('YourComponent', () => {
|
||||
it('renders correctly', () => {
|
||||
const wrapper = mount(YourComponent)
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('handles user interaction', async () => {
|
||||
const wrapper = mount(YourComponent)
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.emitted('event-name')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Test Coverage Expectations
|
||||
|
||||
- **New features**: Must include comprehensive tests
|
||||
- **Bug fixes**: Must include a test that reproduces the bug
|
||||
- **Refactoring**: Existing tests should still pass
|
||||
- **Coverage goal**: Aim for >80% code coverage on new code
|
||||
Tests should be clear, maintainable, and actually validate the intended behavior.
|
||||
|
||||
---
|
||||
|
||||
## Code Quality and Linting
|
||||
## Questions?
|
||||
|
||||
### Backend (Python)
|
||||
|
||||
YTPTube uses **Ruff** for linting and formatting Python code.
|
||||
|
||||
**Running Ruff:**
|
||||
|
||||
```bash
|
||||
# Check for linting issues
|
||||
uv run ruff check app/
|
||||
|
||||
# Auto-fix linting issues
|
||||
uv run ruff check --fix app/
|
||||
|
||||
# Format code
|
||||
uv run ruff format app/
|
||||
|
||||
# Check specific file
|
||||
uv run ruff check app/library/Download.py
|
||||
uv run ruff check --fix app/library/Download.py
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
Ruff configuration is in `pyproject.toml` under `[tool.ruff]`.
|
||||
|
||||
### Frontend (TypeScript/Vue)
|
||||
|
||||
The frontend uses **ESLint** for linting and **TypeScript** for type checking.
|
||||
|
||||
**Running checks:**
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
|
||||
# Type checking
|
||||
pnpm run typecheck
|
||||
|
||||
# Linting
|
||||
pnpm run lint
|
||||
|
||||
# Auto-fix linting issues
|
||||
pnpm run lint:fix
|
||||
```
|
||||
|
||||
### Code Quality Checklist
|
||||
|
||||
Before committing, ensure:
|
||||
|
||||
- ✅ All tests pass (`uv run pytest` and `pnpm test`)
|
||||
- ✅ No linting errors (`ruff check` and `pnpm run lint`)
|
||||
- ✅ TypeScript compilation succeeds (`pnpm run typecheck`)
|
||||
- ✅ Code is formatted properly
|
||||
- ✅ No unused imports or variables
|
||||
- ✅ Type hints are present (Python 3.13+)
|
||||
|
||||
---
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
YTPTube uses a custom shell script-based pre-commit hook that runs all checks in parallel for faster execution.
|
||||
|
||||
### Setting Up Pre-commit Hooks
|
||||
|
||||
1. **Create the pre-commit hook script:**
|
||||
|
||||
Create a file at `.git/hooks/pre-commit` with the following content:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
cd "$REPO_ROOT" || exit 1
|
||||
|
||||
PIDS_FILE=$(mktemp)
|
||||
trap 'rm -f "$PIDS_FILE"' EXIT
|
||||
|
||||
export CI=1
|
||||
run_step() {
|
||||
local label="$1"
|
||||
shift
|
||||
echo "🔹 Starting $label..." >&2
|
||||
(
|
||||
if ! "$@"; then
|
||||
echo "❌ [$label] failed. Commit aborted." >&2
|
||||
exit 1
|
||||
else
|
||||
echo "✅ [$label] passed." >&2
|
||||
fi
|
||||
) &
|
||||
echo $! >>"$PIDS_FILE"
|
||||
}
|
||||
|
||||
run_step "Ruff (Python linter)" uv run ruff check app/
|
||||
run_step "PyTest" uv run pytest app/ -q
|
||||
run_step "ESLint" pnpm --dir ./ui run lint
|
||||
run_step "Vitest" pnpm --dir ./ui run test
|
||||
|
||||
fail=0
|
||||
while read -r pid; do
|
||||
if ! wait "$pid"; then
|
||||
fail=1
|
||||
fi
|
||||
done <"$PIDS_FILE"
|
||||
|
||||
if [ "$fail" -eq 1 ]; then
|
||||
echo "❌ One or more checks failed. Commit aborted." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All checks passed. Commit allowed."
|
||||
exit 0
|
||||
```
|
||||
|
||||
2. **Make the hook executable:**
|
||||
|
||||
```bash
|
||||
chmod +x .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
3. **Test the hook:**
|
||||
|
||||
```bash
|
||||
# Test manually
|
||||
.git/hooks/pre-commit
|
||||
|
||||
# Or make a test commit
|
||||
git commit --allow-empty -m "test: verify pre-commit hook"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Before Submitting
|
||||
|
||||
✅ **Checklist:**
|
||||
|
||||
- [ ] Code is on a feature branch created from `dev`
|
||||
- [ ] All tests pass locally
|
||||
- [ ] Code passes linting and formatting checks
|
||||
- [ ] TypeScript types are properly defined (no `any`) without justification
|
||||
- [ ] New features include tests
|
||||
- [ ] Documentation is updated (if applicable)
|
||||
- [ ] Commit messages are clear and descriptive
|
||||
- [ ] Pre-commit hooks pass
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
### Creating a Pull Request
|
||||
|
||||
1. **Push your branch to GitHub:**
|
||||
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
2. **Open a Pull Request on GitHub**
|
||||
|
||||
3. **Target the `dev` branch** (not `master`)
|
||||
|
||||
4. **Fill out the PR template** with:
|
||||
- Description of changes
|
||||
- Related issue numbers (if applicable)
|
||||
- Testing performed
|
||||
- Screenshots (for UI changes)
|
||||
- Breaking changes (if any)
|
||||
|
||||
### PR Review Process
|
||||
|
||||
1. **Automated Checks**: CI pipeline runs tests and linting
|
||||
2. **Code Review**: Maintainers review your code
|
||||
3. **Feedback**: Address any requested changes
|
||||
4. **Approval**: Once approved, maintainers will merge
|
||||
|
||||
### After Your PR is Merged
|
||||
|
||||
1. **Delete your feature branch** (optional but recommended)
|
||||
2. **Pull the latest `dev` branch:**
|
||||
|
||||
```bash
|
||||
git checkout dev
|
||||
git pull origin dev
|
||||
```
|
||||
|
||||
3. **Celebrate!** Thank you for contributing!
|
||||
|
||||
---
|
||||
|
||||
## Code Style
|
||||
|
||||
### Python Code Style
|
||||
|
||||
- **Line length**: 120 characters
|
||||
- **Indentation**: 4 spaces
|
||||
- **Quotes**: Double quotes for strings
|
||||
- **Type hints**: Use Python 3.13+ type hints
|
||||
- **Imports**: Absolute imports from `app.library`
|
||||
- **Comparison**: Use Yoda comparisons for literals (`"value" == variable`)
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from app.library.Download import Download
|
||||
from app.library.ItemDTO import ItemDTO
|
||||
|
||||
async def process_download(item: ItemDTO) -> bool:
|
||||
"""Process a download item."""
|
||||
try:
|
||||
if "pending" == item.status: # Yoda comparison
|
||||
result = await Download.start(item)
|
||||
return result.success
|
||||
except Exception as e:
|
||||
LOG.error(f"Download failed: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
### TypeScript/Vue Code Style
|
||||
|
||||
- **Type safety**: No `any` types without justification
|
||||
- **Explicit imports**: Import all Vue functions explicitly
|
||||
- **Component structure**: Use `<script setup lang="ts">`
|
||||
- **Props/Emits**: Always type-define
|
||||
|
||||
**Example:**
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { DownloadItem } from '~/types/download'
|
||||
|
||||
const props = defineProps<{
|
||||
items: Array<DownloadItem>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'itemSelected', item: DownloadItem): void
|
||||
}>()
|
||||
|
||||
const selectedItem = ref<DownloadItem | null>(null)
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
### Resources
|
||||
|
||||
- **API Docs**: See [API.md](API.md) for API reference
|
||||
- **FAQ**: Check [FAQ.md](FAQ.md) for common questions
|
||||
- **Issues**: Browse existing [GitHub Issues](https://github.com/arabcoders/ytptube/issues)
|
||||
|
||||
### Ask Questions
|
||||
|
||||
- **GitHub Discussions**: For general questions and discussions
|
||||
- **GitHub Issues**: For bug reports and feature requests
|
||||
- **Pull Requests**: For code review and implementation questions
|
||||
- **Discord**: Join our community on Discord (link in README)
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
When reporting bugs, please include:
|
||||
|
||||
1. **Description**: Clear description of the issue
|
||||
2. **Steps to Reproduce**: Detailed steps to reproduce the bug
|
||||
3. **Expected Behavior**: What you expected to happen
|
||||
4. **Actual Behavior**: What actually happened
|
||||
5. **Environment**: OS, Python version, Node version, etc.
|
||||
6. **Logs**: Relevant error messages or logs
|
||||
7. **Screenshots**: If applicable
|
||||
* Check existing **Issues** and **Discussions** first
|
||||
* Join the project **Discord** for real-time discussion
|
||||
* Be patient, this is a personal project with limited maintenance time
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
By contributing to YTPTube, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
||||
By contributing, you agree that your code will be licensed under the project's **MIT License**.
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to YTPTube! Your efforts help make this project better for everyone.
|
||||
Thank you for respecting this contribution process. It helps maintain project quality and the maintainer's sanity.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ class Condition:
|
|||
extras: dict[str, Any] = field(default_factory=dict)
|
||||
"""Any extra data to store with the condition."""
|
||||
|
||||
enabled: bool = True
|
||||
"""Whether the condition is enabled."""
|
||||
|
||||
def serialize(self) -> dict:
|
||||
return self.__dict__
|
||||
|
||||
|
|
@ -138,6 +141,10 @@ class Conditions(metaclass=Singleton):
|
|||
item["extras"] = {}
|
||||
need_save = True
|
||||
|
||||
if "enabled" not in item:
|
||||
item["enabled"] = True
|
||||
need_save = True
|
||||
|
||||
item: Condition = init_class(Condition, item)
|
||||
|
||||
self._items.append(item)
|
||||
|
|
@ -146,7 +153,7 @@ class Conditions(metaclass=Singleton):
|
|||
continue
|
||||
|
||||
if need_save:
|
||||
LOG.info("Saving conditions due changes.")
|
||||
LOG.warning("Saving conditions due to schema changes.")
|
||||
self.save(self._items)
|
||||
|
||||
return self
|
||||
|
|
@ -299,6 +306,9 @@ class Conditions(metaclass=Singleton):
|
|||
return None
|
||||
|
||||
for item in self.get_all():
|
||||
if not item.enabled:
|
||||
continue
|
||||
|
||||
if not item.filter:
|
||||
LOG.error(f"Filter is empty for '{item.name}'.")
|
||||
continue
|
||||
|
|
@ -330,7 +340,7 @@ class Conditions(metaclass=Singleton):
|
|||
return None
|
||||
|
||||
item = self.get(name)
|
||||
if not item or not item.filter:
|
||||
if not item or not item.enabled or not item.filter:
|
||||
return None
|
||||
|
||||
return item if match_str(item.filter, info) else None
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ FRONTEND_ROUTES: list[str] = [
|
|||
"/logs/",
|
||||
"/conditions/",
|
||||
"/browser/",
|
||||
"/settings/",
|
||||
"/simple/",
|
||||
"/browser/{path:.*}",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ async def conditions_add(request: Request, encoder: Encoder, notify: EventBus) -
|
|||
if not item.get("extras"):
|
||||
item["extras"] = {}
|
||||
|
||||
if "enabled" not in item:
|
||||
item["enabled"] = True
|
||||
|
||||
if not item.get("id", None) or not validate_uuid(item.get("id"), version=4):
|
||||
item["id"] = str(uuid.uuid4())
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@
|
|||
"type": "object",
|
||||
"description": "Any extra data to store with the condition.",
|
||||
"default": {}
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the condition is enabled.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
|
|
@ -43,7 +48,8 @@
|
|||
"filter": "duration>3600",
|
||||
"extras": {
|
||||
"ignore_download": true
|
||||
}
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "b3c4d5e6-7890-3456-1cde-f23456789012",
|
||||
|
|
@ -51,7 +57,8 @@
|
|||
"filter": "categories~='Music'",
|
||||
"extras": {
|
||||
"set_preset": "audio-only"
|
||||
}
|
||||
},
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class TestCondition:
|
|||
# Check defaults
|
||||
assert condition.cli == ""
|
||||
assert condition.extras == {}
|
||||
assert condition.enabled is True
|
||||
|
||||
def test_condition_creation_with_all_fields(self):
|
||||
"""Test creating a condition with all fields specified."""
|
||||
|
|
@ -48,7 +49,7 @@ class TestCondition:
|
|||
extras = {"key": "value", "number": 42}
|
||||
|
||||
condition = Condition(
|
||||
id=test_id, name="full_test", filter="uploader = 'test'", cli="--format best", extras=extras
|
||||
id=test_id, name="full_test", filter="uploader = 'test'", cli="--format best", extras=extras, enabled=False
|
||||
)
|
||||
|
||||
assert condition.id == test_id
|
||||
|
|
@ -56,6 +57,7 @@ class TestCondition:
|
|||
assert condition.filter == "uploader = 'test'"
|
||||
assert condition.cli == "--format best"
|
||||
assert condition.extras == extras
|
||||
assert condition.enabled is False
|
||||
|
||||
def test_condition_serialize(self):
|
||||
"""Test condition serialization to dict."""
|
||||
|
|
@ -209,6 +211,7 @@ class TestConditions:
|
|||
"filter": "duration < 300",
|
||||
"cli": "--format worst",
|
||||
"extras": {"category": "short"},
|
||||
"enabled": True,
|
||||
},
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
|
|
@ -216,6 +219,7 @@ class TestConditions:
|
|||
"filter": "title ~= 'music'",
|
||||
"cli": "--audio-quality 0",
|
||||
"extras": {"type": "audio"},
|
||||
"enabled": False,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -232,10 +236,12 @@ class TestConditions:
|
|||
assert conditions._items[0].filter == "duration < 300"
|
||||
assert conditions._items[0].cli == "--format worst"
|
||||
assert conditions._items[0].extras == {"category": "short"}
|
||||
assert conditions._items[0].enabled is True
|
||||
|
||||
# Check second condition
|
||||
assert conditions._items[1].name == "music_videos"
|
||||
assert conditions._items[1].filter == "title ~= 'music'"
|
||||
assert conditions._items[1].enabled is False
|
||||
|
||||
def test_load_conditions_without_id(self):
|
||||
"""Test loading conditions that don't have ID (should generate ID)."""
|
||||
|
|
@ -279,6 +285,26 @@ class TestConditions:
|
|||
# Should call save due to changes
|
||||
mock_save.assert_called_once()
|
||||
|
||||
def test_load_conditions_without_enabled(self):
|
||||
"""Test loading conditions that don't have enabled field (should default to True)."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
file_path = Path(temp_dir) / "no_enabled_conditions.json"
|
||||
|
||||
test_data = [{"id": str(uuid.uuid4()), "name": "no_enabled_test", "filter": "duration > 60", "extras": {}}]
|
||||
|
||||
file_path.write_text(json.dumps(test_data))
|
||||
|
||||
with patch.object(Conditions, "save") as mock_save:
|
||||
conditions = Conditions(file=file_path)
|
||||
conditions.load()
|
||||
|
||||
# Should have generated enabled=True
|
||||
assert len(conditions._items) == 1
|
||||
assert conditions._items[0].enabled is True
|
||||
|
||||
# Should call save due to changes
|
||||
mock_save.assert_called_once()
|
||||
|
||||
def test_load_invalid_json(self):
|
||||
"""Test loading file with invalid JSON."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
@ -613,6 +639,28 @@ class TestConditions:
|
|||
|
||||
assert result is None
|
||||
|
||||
@patch("app.library.conditions.match_str")
|
||||
def test_match_disabled_condition(self, mock_match_str):
|
||||
"""Test that disabled conditions are skipped during matching."""
|
||||
mock_match_str.return_value = True
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
file_path = Path(temp_dir) / "disabled_match_test.json"
|
||||
conditions = Conditions(file=file_path)
|
||||
|
||||
# Add disabled condition
|
||||
disabled_condition = Condition(name="disabled_test", filter="duration > 60", enabled=False)
|
||||
enabled_condition = Condition(name="enabled_test", filter="duration > 120", enabled=True)
|
||||
conditions._items = [disabled_condition, enabled_condition]
|
||||
|
||||
info_dict = {"duration": 150}
|
||||
result = conditions.match(info_dict)
|
||||
|
||||
# Should skip disabled condition and match enabled one
|
||||
assert result is enabled_condition
|
||||
# Should only call match_str once for enabled condition
|
||||
mock_match_str.assert_called_once_with("duration > 120", info_dict)
|
||||
|
||||
def test_match_empty_filter(self):
|
||||
"""Test matching with condition that has empty filter."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
@ -687,6 +735,21 @@ class TestConditions:
|
|||
|
||||
assert result is None
|
||||
|
||||
def test_single_match_disabled_condition(self):
|
||||
"""Test single matching with disabled condition."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
file_path = Path(temp_dir) / "disabled_single_test.json"
|
||||
conditions = Conditions(file=file_path)
|
||||
|
||||
test_condition = Condition(name="disabled_single", filter="duration > 60", enabled=False)
|
||||
conditions._items = [test_condition]
|
||||
|
||||
info_dict = {"duration": 120}
|
||||
result = conditions.single_match("disabled_single", info_dict)
|
||||
|
||||
# Should return None because condition is disabled
|
||||
assert result is None
|
||||
|
||||
def test_single_match_invalid_inputs(self):
|
||||
"""Test single matching with invalid inputs."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
|
|
|||
|
|
@ -374,3 +374,7 @@ div.is-centered {
|
|||
padding: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.is-rounded-less {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="column is-9-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="name">
|
||||
<span class="icon"><i class="fa-solid fa-tag" /></span>
|
||||
|
|
@ -66,6 +66,26 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-3-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="enabled">
|
||||
<span class="icon"><i class="fa-solid fa-power-off" /></span>
|
||||
Enabled
|
||||
</label>
|
||||
<div class="control is-unselectable">
|
||||
<input id="enabled" type="checkbox" v-model="form.enabled" :disabled="addInProgress"
|
||||
class="switch is-success" />
|
||||
<label for="enabled" class="is-unselectable">
|
||||
{{ form.enabled ? 'Yes' : 'No' }}
|
||||
</label>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span class="is-bold">Whether the condition is enabled.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="filter">
|
||||
|
|
@ -276,6 +296,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import 'assets/css/bulma-switch.css'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import TextareaAutocomplete from '~/components/TextareaAutocomplete.vue'
|
||||
import type { AutoCompleteOptions } from '~/types/autocomplete';
|
||||
|
|
@ -443,6 +464,10 @@ const importItem = async (): Promise<void> => {
|
|||
form.extras = { ...item.extras }
|
||||
}
|
||||
|
||||
if (item.enabled !== undefined) {
|
||||
form.enabled = item.enabled
|
||||
}
|
||||
|
||||
import_string.value = ''
|
||||
showImport.value = false
|
||||
} catch (e: any) {
|
||||
|
|
|
|||
|
|
@ -1,51 +1,39 @@
|
|||
<template>
|
||||
<main class="m-6">
|
||||
|
||||
<div>
|
||||
<template v-if="!simpleMode">
|
||||
<span class="title is-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-cog" /></span>
|
||||
<p class="card-header-title">WebUI Settings</p>
|
||||
</span>
|
||||
</span>
|
||||
<span class="field is-horizontal" />
|
||||
</template>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Page View</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="modal" :class="{ 'is-active': isOpen }">
|
||||
<div class="modal-background" @click="emitter('close')" />
|
||||
<div class="modal-card"
|
||||
:class="{ 'slide-from-right': direction === 'right', 'slide-from-left': direction === 'left' }">
|
||||
<header class="modal-card-head is-rounded-less">
|
||||
<p class="modal-card-title">WebUI Settings</p>
|
||||
<button class="delete" @click="emitter('close')" aria-label="close" />
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="box">
|
||||
<div class="field">
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<label class="label">Page View</label>
|
||||
<div class="control">
|
||||
<input id="view_mode" type="checkbox" class="switch is-success" v-model="simpleMode">
|
||||
<label for="view_mode" class="is-unselectable">
|
||||
{{ simpleMode ? 'Simple View' : 'Regular View' }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"> <i class="fa-solid fa-info-circle" /></span>
|
||||
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
|
||||
The simple view is ideal for non-technical users and mobile devices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="field title is-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-palette" /></span>
|
||||
<p class="card-header-title">Theming</p>
|
||||
</span>
|
||||
</span>
|
||||
<span class="field is-horizontal" />
|
||||
<div class="box">
|
||||
<p class="title is-5 mb-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-palette" /></span>
|
||||
<span>Theming</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label">
|
||||
<label class="label">Color scheme</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="field">
|
||||
<label class="label">Color scheme</label>
|
||||
<div class="control">
|
||||
<label for="auto" class="radio">
|
||||
<input id="auto" type="radio" v-model="selectedTheme" value="auto">
|
||||
|
|
@ -64,15 +52,9 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Show Background</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<label class="label">Show Background</label>
|
||||
<div class="control">
|
||||
<input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable">
|
||||
<label for="random_bg" class="is-unselectable">
|
||||
|
|
@ -80,62 +62,42 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label"></label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="field" v-if="bg_enable">
|
||||
<div class="control">
|
||||
<template v-if="bg_enable">
|
||||
<button @click="$emit('reload_bg')" class="has-text-link" :disabled="isLoading">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa"
|
||||
:class="{ 'fa-spin fa-spinner': isLoading, 'fa-file-image': !isLoading }" /></span>
|
||||
<span>Reload Background</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<button @click="$emit('reload_bg')" class="button is-link is-light is-fullwidth" :disabled="isLoading">
|
||||
<span class="icon"><i class="fa"
|
||||
:class="{ 'fa-spin fa-spinner': isLoading, 'fa-file-image': !isLoading }" /></span>
|
||||
<span>Reload Background</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="bg_enable">
|
||||
<label class="label">Background visibility</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<a class="button is-static">
|
||||
<code>{{ parseFloat(String(1.0 - bg_opacity)).toFixed(2) }}</code>
|
||||
</a>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="range" v-model="bg_opacity" min="0.50" max="1.00" step="0.05">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Background visibility</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-expanded">
|
||||
<a class="button is-static is-small">
|
||||
<code>{{ parseFloat(String(1.0 - bg_opacity)).toFixed(2) }}</code>
|
||||
</a>
|
||||
<div class="box">
|
||||
<p class="title is-5 mb-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-home" /></span>
|
||||
<span>Dashboard</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="control">
|
||||
<input id="random_bg_opacity" style="width: 100%" type="range" v-model="bg_opacity" min="0.50" max="1.00"
|
||||
step="0.05">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="field title is-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-home" /></span>
|
||||
<p class="card-header-title">Dashboard</p>
|
||||
</span>
|
||||
</span>
|
||||
<span class="field is-horizontal" />
|
||||
|
||||
<div class="field is-horizontal" v-if="!simpleMode">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">URL Separator</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="field" v-if="!simpleMode">
|
||||
<label class="label">URL Separator</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="separator">
|
||||
|
|
@ -146,17 +108,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Show Thumbnails</label>
|
||||
</div>
|
||||
|
||||
<div class="field-body">
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<div class="field">
|
||||
<label class="label">Show Thumbnails</label>
|
||||
<div class="control">
|
||||
<input id="show_thumbnail" type="checkbox" class="switch is-success" v-model="show_thumbnail">
|
||||
<label for="show_thumbnail" class="is-unselectable">
|
||||
{{ show_thumbnail ? 'Yes' : 'No' }}
|
||||
|
|
@ -167,17 +122,10 @@
|
|||
Show videos thumbnail if available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal" v-if="show_thumbnail">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Aspect Ratio</label>
|
||||
</div>
|
||||
|
||||
<div class="field-body">
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<div class="field" v-if="show_thumbnail">
|
||||
<label class="label">Aspect Ratio</label>
|
||||
<div class="control">
|
||||
<label for="ratio_16by9" class="radio">
|
||||
<input id="ratio_16by9" type="radio" v-model="thumbnail_ratio" value="is-16by9">
|
||||
<span> 16:9</span>
|
||||
|
|
@ -193,39 +141,27 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="field title is-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-bell" /></span>
|
||||
<p class="card-header-title">Notifications</p>
|
||||
</span>
|
||||
</span>
|
||||
<span class="field is-horizontal" />
|
||||
<div class="box">
|
||||
<p class="title is-5 mb-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-bell" /></span>
|
||||
<span>Notifications</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Show notifications</label>
|
||||
</div>
|
||||
|
||||
<div class="field-body">
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<div class="field">
|
||||
<label class="label">Show notifications</label>
|
||||
<div class="control">
|
||||
<input id="allow_toasts" type="checkbox" class="switch is-success" v-model="allow_toasts">
|
||||
<label for="allow_toasts" class="is-unselectable">
|
||||
{{ allow_toasts ? 'Yes' : 'No' }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal" v-if="allow_toasts">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Notification target</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="field" v-if="allow_toasts">
|
||||
<label class="label">Notification target</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="toast_target" @change="onNotificationTargetChange">
|
||||
|
|
@ -244,15 +180,9 @@
|
|||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal" v-if="allow_toasts && toast_target === 'toast'">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Notifications position</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="field" v-if="allow_toasts && toast_target === 'toast'">
|
||||
<label class="label">Notifications position</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="toast_position">
|
||||
|
|
@ -266,17 +196,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-horizontal" v-if="allow_toasts && toast_target === 'toast'">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">Dismiss notification on click</label>
|
||||
</div>
|
||||
|
||||
<div class="field-body">
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<div class="field" v-if="allow_toasts && toast_target === 'toast'">
|
||||
<label class="label">Dismiss notification on click</label>
|
||||
<div class="control">
|
||||
<input id="dismiss_on_click" type="checkbox" class="switch is-success" v-model="toast_dismiss_on_click">
|
||||
<label for="dismiss_on_click" class="is-unselectable">
|
||||
{{ toast_dismiss_on_click ? 'Yes' : 'No' }}
|
||||
|
|
@ -284,22 +207,31 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import 'assets/css/bulma-switch.css'
|
||||
import { watch, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { POSITION } from 'vue-toastification'
|
||||
import { useConfigStore } from '~/stores/ConfigStore'
|
||||
import { useNotification } from '~/composables/useNotification'
|
||||
import type { notificationTarget } from '~/composables/useNotification'
|
||||
|
||||
defineProps<{ isLoading: boolean }>()
|
||||
const emitter = defineEmits<{ (e: 'reload_bg' | 'close'): void }>()
|
||||
const props = withDefaults(defineProps<{
|
||||
isOpen?: boolean
|
||||
direction?: 'left' | 'right'
|
||||
isLoading?: boolean
|
||||
}>(), {
|
||||
isOpen: false,
|
||||
direction: 'right',
|
||||
isLoading: false
|
||||
})
|
||||
|
||||
const emitter = defineEmits<{ (e: 'close' | 'reload_bg'): void }>()
|
||||
|
||||
const config = useConfigStore()
|
||||
const notification = useNotification()
|
||||
|
|
@ -317,14 +249,27 @@ const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',
|
|||
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
|
||||
const isSecureContext = ref<boolean>(false)
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if ('Escape' === e.key && props.isOpen) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
emitter('close')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isSecureContext.value = window.isSecureContext
|
||||
await nextTick()
|
||||
|
||||
if ('browser' === toast_target.value && !isSecureContext.value) {
|
||||
toast_target.value = 'toast'
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', handleKeydown))
|
||||
|
||||
const onNotificationTargetChange = async (): Promise<void> => {
|
||||
if ('browser' === toast_target.value) {
|
||||
const permission = await notification.requestBrowserPermission()
|
||||
|
|
@ -335,5 +280,74 @@ const onNotificationTargetChange = async (): Promise<void> => {
|
|||
}
|
||||
}
|
||||
|
||||
watch(simpleMode, async () => emitter('close'))
|
||||
watch(() => props.isOpen, isOpen => {
|
||||
if (isOpen) {
|
||||
document.body.classList.add('settings-panel-open')
|
||||
} else {
|
||||
document.body.classList.remove('settings-panel-open')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-card.slide-from-right {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
margin: 0;
|
||||
width: 600px;
|
||||
max-width: 90vw;
|
||||
transition: transform 0.3s ease;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.modal.is-active .modal-card.slide-from-right {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.modal-card.slide-from-left {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
margin: 0;
|
||||
width: 600px;
|
||||
max-width: 90vw;
|
||||
transition: transform 0.3s ease;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.modal.is-active .modal-card.slide-from-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.modal-card-body {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.modal-card.slide-from-right,
|
||||
.modal-card.slide-from-left {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
:global(body.settings-panel-open) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#main_container {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
:global(.settings-open #main_container) {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<div class="basic-wrapper">
|
||||
<div class="basic-wrapper" :class="{ 'settings-open': settingsOpen }">
|
||||
<div class="form-container" :class="{ 'is-centered': shouldCenterForm }">
|
||||
<section class="download-form box">
|
||||
<form class="download-form__body" autocomplete="off" @submit.prevent="addDownload">
|
||||
<label class="label" for="download-url">
|
||||
{{ greetingMessage }}
|
||||
<template v-if="!isMobile">{{ greetingMessage }}</template>
|
||||
<template v-else>What would you like to download?</template>
|
||||
<span class="is-pulled-right">
|
||||
<span class="icon is-pointer" :class="connectionStatusColor" @click="$emit('show_settings')"
|
||||
v-tooltip="'WebUI Settings'">
|
||||
|
|
@ -106,7 +107,9 @@
|
|||
</figure>
|
||||
<div class="media-content is-grid">
|
||||
<p class="title is-6 mb-0 queue-title">
|
||||
<NuxtLink target="_blank" :href="entry.item.url">{{ entry.item.title }}</NuxtLink>
|
||||
<NuxtLink target="_blank" :href="entry.item.url" v-tooltip="entry.item.title">
|
||||
{{ entry.item.title || 'Untitled' }}
|
||||
</NuxtLink>
|
||||
</p>
|
||||
<div class="field is-grouped is-unselectable">
|
||||
<div class="control">
|
||||
|
|
@ -205,7 +208,6 @@
|
|||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="closePlayer"></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -225,10 +227,19 @@ import { ag, encodePath, formatTime, makeDownload, request, stripPath, ucFirst,
|
|||
|
||||
defineEmits<{ (e: 'show_settings'): void }>()
|
||||
|
||||
withDefaults(defineProps<{
|
||||
settingsOpen?: boolean
|
||||
}>(), {
|
||||
settingsOpen: false
|
||||
})
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const socketStore = useSocketStore()
|
||||
const toast = useNotification()
|
||||
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
||||
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 })
|
||||
|
||||
const { app, paused, presets } = storeToRefs(configStore)
|
||||
const { queue, history } = storeToRefs(stateStore)
|
||||
|
|
@ -241,12 +252,9 @@ const formUrl = ref<string>('')
|
|||
const formPreset = ref<{ preset: string }>({ preset: app.value.default_preset || '' })
|
||||
const addInProgress = ref<boolean>(false)
|
||||
const showExtras = ref<boolean>(false)
|
||||
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
|
||||
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
|
||||
|
||||
const paginationInfo = computed(() => stateStore.getPagination())
|
||||
|
||||
const queueItems = computed<StoreItem[]>(() => Object.values(queue.value ?? {}).slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)))
|
||||
|
||||
const historyEntries = computed<StoreItem[]>(() => {
|
||||
const items = Object.values(history.value ?? {})
|
||||
return items.slice().sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime())
|
||||
|
|
@ -654,6 +662,10 @@ onMounted(async () => {
|
|||
if (route.query?.simple !== undefined) {
|
||||
const simpleMode = useStorage<boolean>('simple_mode', configStore.app.simple_mode || false)
|
||||
simpleMode.value = ['true', '1', 'yes', 'on'].includes(route.query.simple as string)
|
||||
await nextTick()
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete('simple')
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
|
||||
if (socketStore.isConnected && !paginationInfo.value.isLoaded) {
|
||||
|
|
@ -718,6 +730,17 @@ useIntersectionObserver(loadMoreTrigger, ([entry]) => {
|
|||
align-items: center;
|
||||
gap: 2.5rem;
|
||||
padding: 2rem 1rem 4rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.basic-wrapper.settings-open {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.basic-wrapper.settings-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
|
|
|
|||
|
|
@ -2,38 +2,16 @@
|
|||
|
||||
<template v-if="simpleMode">
|
||||
<Connection :status="socket.connectionStatus" @reconnect="() => socket.reconnect()" />
|
||||
<Simple @show_settings="() => show_settings = true" />
|
||||
<Simple @show_settings="() =>show_settings = true"
|
||||
:class="{ 'settings-open': show_settings }" />
|
||||
</template>
|
||||
|
||||
<template v-if="show_settings">
|
||||
<Modal @close="closeSettings()"
|
||||
:content-class="isMobile ? 'modal-content-max is-overflow-scroll ' : 'modal-content-max'">
|
||||
<div class="columns is-multiline" style="width:100%">
|
||||
<div class="column is-12">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">WebUI Settings</p>
|
||||
<span class="card-header-icon">
|
||||
<span class="icon"><i class="fas fa-cog" /></span>
|
||||
</span>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12">
|
||||
<settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)"
|
||||
@close="closeSettings()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<SettingsPanel :isOpen="show_settings" :isLoading="loadingImage" @close="closeSettings()"
|
||||
@reload_bg="() => loadImage(true)" direction="right" />
|
||||
|
||||
<template v-if="!simpleMode">
|
||||
<Shutdown v-if="app_shutdown" />
|
||||
<div id="main_container" class="container" v-else>
|
||||
<div id="main_container" class="container" :class="{ 'settings-open': show_settings }" v-else>
|
||||
<NewVersion v-if="newVersionIsAvailable" />
|
||||
<Connection :status="socket.connectionStatus" @reconnect="() => socket.reconnect()" />
|
||||
<nav class="navbar is-mobile is-dark">
|
||||
|
|
@ -142,11 +120,11 @@
|
|||
</div>
|
||||
|
||||
<div class="navbar-item">
|
||||
<NuxtLink class="button is-dark " :class="{ 'has-tooltip-bottom mr-4': !isMobile }" to="/settings"
|
||||
@click.prevent="(e: MouseEvent) => changeRoute(e)">
|
||||
<button class="button is-dark " :class="{ 'has-tooltip-bottom mr-4': !isMobile }"
|
||||
@click="show_settings = !show_settings">
|
||||
<span class="icon"><i class="fas fa-cog" /></span>
|
||||
<span v-if="isMobile">WebUI Settings</span>
|
||||
</NuxtLink>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -226,7 +204,6 @@ import Simple from '~/components/Simple.vue'
|
|||
import Shutdown from '~/components/shutdown.vue'
|
||||
import Markdown from '~/components/Markdown.vue'
|
||||
import Connection from '~/components/Connection.vue'
|
||||
import Settings from "~/pages/settings.vue";
|
||||
|
||||
const Year = new Date().getFullYear()
|
||||
const selectedTheme = useStorage('theme', 'auto')
|
||||
|
|
@ -241,7 +218,6 @@ const isMobile = useMediaQuery({ maxWidth: 1024 })
|
|||
const app_shutdown = ref<boolean>(false)
|
||||
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
|
||||
const show_settings = ref(false)
|
||||
|
||||
const doc = ref<{ file: string }>({ file: '' })
|
||||
|
||||
const applyPreferredColorScheme = (scheme: string) => {
|
||||
|
|
@ -525,3 +501,23 @@ const connectionStatusColor = computed(() => {
|
|||
|
||||
const scrollToTop = () => document.getElementById('top')?.scrollIntoView({ behavior: 'smooth' });
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#main_container,
|
||||
.basic-wrapper {
|
||||
transition: transform 0.3s ease, margin-right 0.3s ease;
|
||||
}
|
||||
|
||||
#main_container.settings-open,
|
||||
.basic-wrapper.settings-open {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
#main_container.settings-open,
|
||||
.basic-wrapper.settings-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -50,9 +50,19 @@
|
|||
<header class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block" v-text="cond.name" />
|
||||
<div class="card-header-icon">
|
||||
<a class="has-text-info" v-tooltip="'Export item.'" @click.prevent="exportItem(cond)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</a>
|
||||
<div class="field is-grouped">
|
||||
<div class="control" @click="toggleEnabled(cond)">
|
||||
<span class="icon" :class="cond.enabled ? 'has-text-success' : 'has-text-danger'"
|
||||
v-tooltip="`Condition is ${cond.enabled !== false ? 'enabled' : 'disabled'}. Click to toggle.`">
|
||||
<i class="fa-solid" :class="cond.enabled ? 'fa-check-circle' : 'fa-times-circle'" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="has-text-info" v-tooltip="'Export item'" @click.prevent="exportItem(cond)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content is-flex-grow-1">
|
||||
|
|
@ -193,11 +203,11 @@ const updateItems = async (newItems: ConditionItem[]): Promise<boolean> => {
|
|||
try {
|
||||
addInProgress.value = true
|
||||
|
||||
const validItems = newItems.map(({ id, name, filter, cli, extras }) => {
|
||||
const validItems = newItems.map(({ id, name, filter, cli, extras, enabled }) => {
|
||||
if (!name || !filter) {
|
||||
throw new Error('Name and filter are required.')
|
||||
}
|
||||
return { id, name, filter, cli, extras }
|
||||
return { id, name, filter, cli, extras, enabled }
|
||||
})
|
||||
|
||||
const response = await request('/api/conditions', {
|
||||
|
|
@ -273,6 +283,26 @@ const editItem = (_item: ConditionItem): void => {
|
|||
toggleForm.value = true
|
||||
}
|
||||
|
||||
const toggleEnabled = async (cond: ConditionItem): Promise<void> => {
|
||||
const index = items.value.findIndex(t => t?.id === cond.id)
|
||||
if (-1 === index) {
|
||||
toast.error('Item not found.')
|
||||
return
|
||||
}
|
||||
|
||||
const item = items.value[index]
|
||||
if (!item) {
|
||||
toast.error('Item not found.')
|
||||
return
|
||||
}
|
||||
|
||||
item.enabled = !item.enabled
|
||||
const status = await updateItems(items.value)
|
||||
if (status) {
|
||||
toast[item.enabled ? 'success' : 'warning'](`Condition is ${item.enabled ? 'enabled' : 'disabled'}.`)
|
||||
}
|
||||
}
|
||||
|
||||
const exportItem = (cond: ConditionItem): void => {
|
||||
const clone: Partial<ImportedConditionItem> = JSON.parse(JSON.stringify(cond))
|
||||
delete clone.id
|
||||
|
|
|
|||
|
|
@ -163,12 +163,16 @@ watch(() => route.query.tab, (newTab) => {
|
|||
activeTab.value = newTab as TabType
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const route = useRoute()
|
||||
|
||||
if (route.query?.simple !== undefined) {
|
||||
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
|
||||
simpleMode.value = ['true', '1', 'yes', 'on'].includes(route.query.simple as string)
|
||||
await nextTick()
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete('simple')
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
|
||||
activeTab.value = getInitialTab()
|
||||
|
|
|
|||
12
ui/app/pages/simple.vue
Normal file
12
ui/app/pages/simple.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template></template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
const simpleMode = useStorage<boolean>('simple_mode', true)
|
||||
|
||||
onMounted(async () => {
|
||||
simpleMode.value = true
|
||||
await nextTick()
|
||||
await navigateTo('/')
|
||||
})
|
||||
</script>
|
||||
1
ui/app/types/conditions.d.ts
vendored
1
ui/app/types/conditions.d.ts
vendored
|
|
@ -4,6 +4,7 @@ export type ConditionItem = {
|
|||
filter: string
|
||||
cli: string
|
||||
extras: Record<string, any>
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ImportedConditionItem = ConditionItem & {
|
||||
|
|
|
|||
Loading…
Reference in a new issue