mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-01 23:42:22 +00:00
Merge branch 'develop' into feature/newarch
This commit is contained in:
478
.github/workflows/artifact-comment.yml
vendored
Normal file
478
.github/workflows/artifact-comment.yml
vendored
Normal file
@@ -0,0 +1,478 @@
|
||||
name: 📝 Artifact Comment on PR
|
||||
|
||||
concurrency:
|
||||
group: artifact-comment-${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual testing
|
||||
pull_request: # Show in PR checks and provide status updates
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_run: # Triggered when build workflows complete
|
||||
workflows:
|
||||
- "🏗️ Build Apps"
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
comment-artifacts:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
|
||||
name: 📦 Post Build Artifacts
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: 🔍 Get PR and Artifacts
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
// Check if we're running from a fork (more precise detection)
|
||||
const targetRepo = context.repo.owner + '/' + context.repo.repo;
|
||||
const prHeadRepo = context.payload.pull_request?.head?.repo?.full_name;
|
||||
const workflowHeadRepo = context.payload.workflow_run?.head_repository?.full_name;
|
||||
|
||||
// For debugging
|
||||
console.log('🔍 Repository detection:');
|
||||
console.log('- Target repository:', targetRepo);
|
||||
console.log('- PR head repository:', prHeadRepo || 'N/A');
|
||||
console.log('- Workflow head repository:', workflowHeadRepo || 'N/A');
|
||||
console.log('- Event name:', context.eventName);
|
||||
|
||||
// Only skip if it's actually a different repository (fork)
|
||||
const isFromFork = prHeadRepo && prHeadRepo !== targetRepo;
|
||||
const workflowFromFork = workflowHeadRepo && workflowHeadRepo !== targetRepo;
|
||||
|
||||
if (isFromFork || workflowFromFork) {
|
||||
console.log('🚫 Workflow running from fork - skipping comment creation to avoid permission errors');
|
||||
console.log('Fork repository:', prHeadRepo || workflowHeadRepo);
|
||||
console.log('Target repository:', targetRepo);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Same repository - proceeding with comment creation'); // Handle repository_dispatch, pull_request, and manual dispatch events
|
||||
let pr;
|
||||
let targetCommitSha;
|
||||
|
||||
if (context.eventName === 'workflow_run') {
|
||||
// Find PR associated with this workflow run commit
|
||||
console.log('Workflow run event:', context.payload.workflow_run.name);
|
||||
|
||||
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.payload.workflow_run.head_sha
|
||||
});
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
console.log('No pull request found for commit:', context.payload.workflow_run.head_sha);
|
||||
return;
|
||||
}
|
||||
|
||||
pr = pullRequests[0];
|
||||
targetCommitSha = context.payload.workflow_run.head_sha;
|
||||
|
||||
} else if (context.eventName === 'pull_request') {
|
||||
// Direct PR event
|
||||
pr = context.payload.pull_request;
|
||||
targetCommitSha = pr.head.sha;
|
||||
|
||||
} else if (context.eventName === 'workflow_dispatch') {
|
||||
// For manual testing, try to find PR for current branch/commit
|
||||
console.log('Manual workflow dispatch triggered');
|
||||
|
||||
// First, try to find PRs associated with current commit
|
||||
try {
|
||||
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.sha
|
||||
});
|
||||
|
||||
if (pullRequests.length > 0) {
|
||||
pr = pullRequests[0];
|
||||
targetCommitSha = pr.head.sha;
|
||||
console.log(`Found PR #${pr.number} for commit ${context.sha.substring(0, 7)}`);
|
||||
} else {
|
||||
// Fallback: get latest open PR
|
||||
const { data: openPRs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
sort: 'updated',
|
||||
direction: 'desc',
|
||||
per_page: 1
|
||||
});
|
||||
|
||||
if (openPRs.length > 0) {
|
||||
pr = openPRs[0];
|
||||
targetCommitSha = pr.head.sha;
|
||||
console.log(`Using latest open PR #${pr.number} for manual testing`);
|
||||
} else {
|
||||
console.log('No open PRs found for manual testing');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error finding PR for manual testing:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('Unsupported event type:', context.eventName);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Processing PR #${pr.number} for commit ${targetCommitSha.substring(0, 7)}`);
|
||||
|
||||
// Get all recent workflow runs for this PR to collect artifacts from multiple builds
|
||||
const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head_sha: targetCommitSha,
|
||||
per_page: 30
|
||||
});
|
||||
|
||||
// Filter for build workflows only, include active runs even if marked as cancelled
|
||||
const buildRuns = workflowRuns.workflow_runs
|
||||
.filter(run =>
|
||||
(run.name.includes('Build Apps') ||
|
||||
run.name.includes('Android APK Build') ||
|
||||
run.name.includes('iOS IPA Build'))
|
||||
)
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
|
||||
console.log(`Found ${buildRuns.length} non-cancelled build workflow runs for this commit`);
|
||||
|
||||
// Log current status of each build for debugging
|
||||
buildRuns.forEach(run => {
|
||||
console.log(`- ${run.name}: ${run.status} (${run.conclusion || 'no conclusion yet'}) - Created: ${run.created_at}`);
|
||||
});
|
||||
|
||||
// Collect artifacts and statuses from builds - prioritize active runs over completed ones
|
||||
let allArtifacts = [];
|
||||
let buildStatuses = {};
|
||||
|
||||
// Get the most relevant run for each workflow type (prioritize active over cancelled)
|
||||
const findBestRun = (nameFilter) => {
|
||||
const matchingRuns = buildRuns.filter(run => run.name.includes(nameFilter));
|
||||
|
||||
// First try to find an in-progress run
|
||||
const inProgressRun = matchingRuns.find(run => run.status === 'in_progress');
|
||||
if (inProgressRun) return inProgressRun;
|
||||
|
||||
// Then try to find a queued run
|
||||
const queuedRun = matchingRuns.find(run => run.status === 'queued');
|
||||
if (queuedRun) return queuedRun;
|
||||
|
||||
// Check if the workflow is completed but has non-cancelled jobs
|
||||
const completedRuns = matchingRuns.filter(run => run.status === 'completed');
|
||||
for (const run of completedRuns) {
|
||||
// We'll check individual jobs later to see if they're actually running
|
||||
if (run.conclusion !== 'cancelled') {
|
||||
return run;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally fall back to most recent run (even if cancelled at workflow level)
|
||||
return matchingRuns[0]; // Already sorted by most recent first
|
||||
};
|
||||
|
||||
const latestAppsRun = findBestRun('Build Apps');
|
||||
const latestAndroidRun = findBestRun('Android APK Build');
|
||||
const latestIOSRun = findBestRun('iOS IPA Build');
|
||||
|
||||
// For the consolidated workflow, get individual job statuses
|
||||
if (latestAppsRun) {
|
||||
console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
|
||||
|
||||
try {
|
||||
// Get all jobs for this workflow run
|
||||
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestAppsRun.id
|
||||
});
|
||||
|
||||
console.log(`Found ${jobs.jobs.length} jobs in workflow run`);
|
||||
jobs.jobs.forEach(job => {
|
||||
console.log(`- Job: ${job.name} | Status: ${job.status} | Conclusion: ${job.conclusion || 'none'}`);
|
||||
});
|
||||
|
||||
// Check if we have any actually running jobs (not cancelled)
|
||||
const activeJobs = jobs.jobs.filter(job =>
|
||||
job.status === 'in_progress' ||
|
||||
job.status === 'queued' ||
|
||||
(job.status === 'completed' && job.conclusion !== 'cancelled')
|
||||
);
|
||||
|
||||
console.log(`Found ${activeJobs.length} active (non-cancelled) jobs out of ${jobs.jobs.length} total jobs`);
|
||||
|
||||
// If no jobs are actually running, skip this workflow
|
||||
if (activeJobs.length === 0 && latestAppsRun.conclusion === 'cancelled') {
|
||||
console.log('All jobs are cancelled, skipping this workflow run');
|
||||
return; // Exit early
|
||||
}
|
||||
|
||||
// Map job names to our build targets
|
||||
const jobMappings = {
|
||||
'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
|
||||
'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
|
||||
'iOS Phone': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone']
|
||||
};
|
||||
|
||||
// Create individual status for each job
|
||||
for (const [platform, jobNames] of Object.entries(jobMappings)) {
|
||||
const job = jobs.jobs.find(j =>
|
||||
jobNames.some(name => j.name.includes(name) || j.name === name)
|
||||
);
|
||||
|
||||
if (job) {
|
||||
buildStatuses[platform] = {
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
conclusion: job.conclusion,
|
||||
url: job.html_url,
|
||||
runId: latestAppsRun.id,
|
||||
created_at: job.started_at || latestAppsRun.created_at
|
||||
};
|
||||
console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`);
|
||||
} else {
|
||||
console.log(`No job found for ${platform}, using workflow status as fallback`);
|
||||
buildStatuses[platform] = {
|
||||
name: latestAppsRun.name,
|
||||
status: latestAppsRun.status,
|
||||
conclusion: latestAppsRun.conclusion,
|
||||
url: latestAppsRun.html_url,
|
||||
runId: latestAppsRun.id,
|
||||
created_at: latestAppsRun.created_at
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message);
|
||||
// Fallback to workflow-level status
|
||||
buildStatuses['Android Phone'] = buildStatuses['Android TV'] = buildStatuses['iOS Phone'] = {
|
||||
name: latestAppsRun.name,
|
||||
status: latestAppsRun.status,
|
||||
conclusion: latestAppsRun.conclusion,
|
||||
url: latestAppsRun.html_url,
|
||||
runId: latestAppsRun.id,
|
||||
created_at: latestAppsRun.created_at
|
||||
};
|
||||
}
|
||||
|
||||
// Collect artifacts if any job has completed successfully
|
||||
if (latestAppsRun.status === 'completed' ||
|
||||
Object.values(buildStatuses).some(status => status.conclusion === 'success')) {
|
||||
try {
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestAppsRun.id
|
||||
});
|
||||
allArtifacts.push(...artifacts.artifacts);
|
||||
} catch (error) {
|
||||
console.log(`Failed to get apps artifacts for run ${latestAppsRun.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to separate workflows (for backward compatibility)
|
||||
if (latestAndroidRun) {
|
||||
buildStatuses['Android'] = {
|
||||
name: latestAndroidRun.name,
|
||||
status: latestAndroidRun.status,
|
||||
conclusion: latestAndroidRun.conclusion,
|
||||
url: latestAndroidRun.html_url,
|
||||
runId: latestAndroidRun.id,
|
||||
created_at: latestAndroidRun.created_at
|
||||
};
|
||||
|
||||
if (latestAndroidRun.conclusion === 'success') {
|
||||
try {
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestAndroidRun.id
|
||||
});
|
||||
allArtifacts.push(...artifacts.artifacts);
|
||||
} catch (error) {
|
||||
console.log(`Failed to get Android artifacts for run ${latestAndroidRun.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (latestIOSRun) {
|
||||
buildStatuses['iOS'] = {
|
||||
name: latestIOSRun.name,
|
||||
status: latestIOSRun.status,
|
||||
conclusion: latestIOSRun.conclusion,
|
||||
url: latestIOSRun.html_url,
|
||||
runId: latestIOSRun.id,
|
||||
created_at: latestIOSRun.created_at
|
||||
};
|
||||
|
||||
if (latestIOSRun.conclusion === 'success') {
|
||||
try {
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestIOSRun.id
|
||||
});
|
||||
allArtifacts.push(...artifacts.artifacts);
|
||||
} catch (error) {
|
||||
console.log(`Failed to get iOS artifacts for run ${latestIOSRun.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
|
||||
|
||||
// Debug: Show which workflow we're using and its status
|
||||
if (latestAppsRun) {
|
||||
console.log(`Using consolidated workflow: ${latestAppsRun.name} (${latestAppsRun.status}/${latestAppsRun.conclusion})`);
|
||||
} else {
|
||||
console.log(`Using separate workflows - Android: ${latestAndroidRun?.name || 'none'}, iOS: ${latestIOSRun?.name || 'none'}`);
|
||||
}
|
||||
|
||||
// Debug: List all artifacts found
|
||||
allArtifacts.forEach(artifact => {
|
||||
console.log(`- Artifact: ${artifact.name} (from run ${artifact.workflow_run.id})`);
|
||||
});
|
||||
|
||||
// Build comment body with progressive status for individual builds
|
||||
let commentBody = `## 🔧 Build Status for PR #${pr.number}\n\n`;
|
||||
commentBody += `🔗 **Commit**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\n\n`; // Progressive build status and downloads table
|
||||
commentBody += `### 📦 Build Artifacts\n\n`;
|
||||
commentBody += `| Platform | Device | Status | Download |\n`;
|
||||
commentBody += `|----------|--------|--------|---------|\n`;
|
||||
|
||||
// Process each expected build target individually
|
||||
const buildTargets = [
|
||||
{ name: 'Android Phone', platform: '🤖', device: '📱', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
|
||||
{ name: 'Android TV', platform: '🤖', device: '📺', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
|
||||
{ name: 'iOS Phone', platform: '🍎', device: '📱', statusKey: 'iOS Phone', artifactPattern: /ios.*phone/i },
|
||||
{ name: 'iOS TV', platform: '🍎', device: '📺', statusKey: 'iOS TV', artifactPattern: /ios.*tv/i }
|
||||
];
|
||||
|
||||
for (const target of buildTargets) {
|
||||
// Find matching job status directly
|
||||
const matchingStatus = buildStatuses[target.statusKey];
|
||||
|
||||
// Find matching artifact
|
||||
const matchingArtifact = allArtifacts.find(artifact =>
|
||||
target.artifactPattern.test(artifact.name)
|
||||
);
|
||||
|
||||
let status = '⏳ Pending';
|
||||
let downloadLink = '*Waiting for build...*';
|
||||
|
||||
// Special case for iOS TV - show as disabled
|
||||
if (target.name === 'iOS TV') {
|
||||
status = '💤 Disabled';
|
||||
downloadLink = '*Disabled for now*';
|
||||
} else if (matchingStatus) {
|
||||
if (matchingStatus.conclusion === 'success' && matchingArtifact) {
|
||||
status = '✅ Complete';
|
||||
const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`;
|
||||
const fileType = target.name.includes('Android') ? 'APK' : 'IPA';
|
||||
downloadLink = `[📥 Download ${fileType}](${directLink})`;
|
||||
} else if (matchingStatus.conclusion === 'failure') {
|
||||
status = `❌ [Failed](${matchingStatus.url})`;
|
||||
downloadLink = '*Build failed*';
|
||||
} else if (matchingStatus.conclusion === 'cancelled') {
|
||||
status = `⚪ [Cancelled](${matchingStatus.url})`;
|
||||
downloadLink = '*Build cancelled*';
|
||||
} else if (matchingStatus.status === 'in_progress') {
|
||||
status = `🔄 [Building...](${matchingStatus.url})`;
|
||||
downloadLink = '*Build in progress...*';
|
||||
} else if (matchingStatus.status === 'queued') {
|
||||
status = `⏳ [Queued](${matchingStatus.url})`;
|
||||
downloadLink = '*Waiting to start...*';
|
||||
} else if (matchingStatus.status === 'completed' && !matchingStatus.conclusion) {
|
||||
// Workflow completed but conclusion not yet available (rare edge case)
|
||||
status = `🔄 [Finishing...](${matchingStatus.url})`;
|
||||
downloadLink = '*Finalizing build...*';
|
||||
} else if (matchingStatus.status === 'completed' && matchingStatus.conclusion === 'success' && !matchingArtifact) {
|
||||
// Build succeeded but artifacts not yet available
|
||||
status = `⏳ [Processing artifacts...](${matchingStatus.url})`;
|
||||
downloadLink = '*Preparing download...*';
|
||||
} else {
|
||||
// Fallback for any unexpected states
|
||||
status = `❓ [${matchingStatus.status}/${matchingStatus.conclusion || 'pending'}](${matchingStatus.url})`;
|
||||
downloadLink = `*Status: ${matchingStatus.status}, Conclusion: ${matchingStatus.conclusion || 'pending'}*`;
|
||||
}
|
||||
}
|
||||
|
||||
commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`;
|
||||
}
|
||||
|
||||
commentBody += `\n`;
|
||||
|
||||
// Show installation instructions if we have any artifacts
|
||||
if (allArtifacts.length > 0) {
|
||||
commentBody += `### 🔧 Installation Instructions\n\n`;
|
||||
commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`;
|
||||
commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`;
|
||||
commentBody += `> ⚠️ **Note**: Artifacts expire in 7 days from build date\n\n`;
|
||||
} else {
|
||||
commentBody += `⏳ **Builds are starting up...** This comment will update automatically as each build completes.\n\n`;
|
||||
}
|
||||
|
||||
commentBody += `<sub>*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*</sub>`;
|
||||
commentBody += `\n<!-- streamyfin-artifact-comment -->`;
|
||||
|
||||
// Try to find existing bot comment to update (with permission check)
|
||||
try {
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('<!-- streamyfin-artifact-comment -->')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
console.log(`✅ Updated comment ${botComment.id} on PR #${pr.number}`);
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: commentBody
|
||||
});
|
||||
console.log(`✅ Created new comment on PR #${pr.number}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 403) {
|
||||
console.log('🚫 Permission denied - likely running from a fork. Skipping comment creation.');
|
||||
console.log('Error details:', error.message);
|
||||
|
||||
// Log the build status instead of commenting
|
||||
console.log('📊 Build Status Summary:');
|
||||
for (const target of buildTargets) {
|
||||
const matchingStatus = buildStatuses[target.statusKey];
|
||||
if (matchingStatus) {
|
||||
console.log(`- ${target.name}: ${matchingStatus.status}/${matchingStatus.conclusion || 'none'}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Re-throw other errors
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
93
.github/workflows/build-android.yml
vendored
93
.github/workflows/build-android.yml
vendored
@@ -1,93 +0,0 @@
|
||||
name: 🤖 Android APK Build (Phone + TV)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [develop, master]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
if: (!contains(github.event.head_commit.message, '[skip ci]'))
|
||||
runs-on: ubuntu-24.04
|
||||
name: 🏗️ Build Android APK
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [phone, tv]
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
show-progress: false
|
||||
|
||||
- name: 🍞 Setup Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 💾 Cache Bun dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-bun-develop
|
||||
${{ runner.os }}-bun-develop
|
||||
|
||||
- name: 📦 Install dependencies and reload submodules
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run submodule-reload
|
||||
|
||||
- name: 💾 Cache Gradle global
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-develop
|
||||
|
||||
- name: 🛠️ Generate project files
|
||||
run: |
|
||||
if [ "${{ matrix.target }}" = "tv" ]; then
|
||||
bun run prebuild:tv
|
||||
else
|
||||
bun run prebuild
|
||||
fi
|
||||
|
||||
- name: 💾 Cache project Gradle (.gradle)
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: android/.gradle
|
||||
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-android-gradle-develop
|
||||
|
||||
- name: 🚀 Build APK
|
||||
env:
|
||||
EXPO_TV: ${{ matrix.target == 'tv' && 1 || 0 }}
|
||||
run: bun run build:android:local
|
||||
|
||||
- name: 📅 Set date tag
|
||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: 📤 Upload APK artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: streamyfin-android-${{ matrix.target }}-apk-${{ env.DATE_TAG }}
|
||||
path: |
|
||||
android/app/build/outputs/apk/release/*.apk
|
||||
retention-days: 7
|
||||
280
.github/workflows/build-apps.yml
vendored
Normal file
280
.github/workflows/build-apps.yml
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
name: 🏗️ Build Apps
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [develop, master]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
|
||||
jobs:
|
||||
build-android-phone:
|
||||
if: (!contains(github.event.head_commit.message, '[skip ci]'))
|
||||
runs-on: ubuntu-24.04
|
||||
name: 🤖 Build Android APK (Phone)
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
show-progress: false
|
||||
|
||||
- name: 🍞 Setup Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 💾 Cache Bun dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-bun-develop
|
||||
${{ runner.os }}-bun-develop
|
||||
|
||||
- name: 📦 Install dependencies and reload submodules
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run submodule-reload
|
||||
|
||||
- name: 💾 Cache Gradle global
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: 🛠️ Generate project files
|
||||
run: bun run prebuild
|
||||
|
||||
- name: 💾 Cache project Gradle (.gradle)
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: android/.gradle
|
||||
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-android-gradle-develop
|
||||
|
||||
- name: 🚀 Build APK
|
||||
env:
|
||||
EXPO_TV: 0
|
||||
run: bun run build:android:local
|
||||
|
||||
- name: 📅 Set date tag
|
||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: 📤 Upload APK artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: streamyfin-android-phone-apk-${{ env.DATE_TAG }}
|
||||
path: |
|
||||
android/app/build/outputs/apk/release/*.apk
|
||||
retention-days: 7
|
||||
|
||||
build-android-tv:
|
||||
if: (!contains(github.event.head_commit.message, '[skip ci]'))
|
||||
runs-on: ubuntu-24.04
|
||||
name: 🤖 Build Android APK (TV)
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
show-progress: false
|
||||
|
||||
- name: 🍞 Setup Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 💾 Cache Bun dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-bun-develop
|
||||
${{ runner.os }}-bun-develop
|
||||
|
||||
- name: 📦 Install dependencies and reload submodules
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run submodule-reload
|
||||
|
||||
- name: 💾 Cache Gradle global
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: 🛠️ Generate project files
|
||||
run: bun run prebuild:tv
|
||||
|
||||
- name: 💾 Cache project Gradle (.gradle)
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: android/.gradle
|
||||
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-android-gradle-develop
|
||||
|
||||
- name: 🚀 Build APK
|
||||
env:
|
||||
EXPO_TV: 1
|
||||
run: bun run build:android:local
|
||||
|
||||
- name: 📅 Set date tag
|
||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: 📤 Upload APK artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: streamyfin-android-tv-apk-${{ env.DATE_TAG }}
|
||||
path: |
|
||||
android/app/build/outputs/apk/release/*.apk
|
||||
retention-days: 7
|
||||
|
||||
build-ios-phone:
|
||||
if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
|
||||
runs-on: macos-15
|
||||
name: 🍎 Build iOS IPA (Phone)
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
show-progress: false
|
||||
|
||||
- name: 🍞 Setup Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 💾 Cache Bun dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-cache
|
||||
|
||||
- name: 📦 Install dependencies and reload submodules
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run submodule-reload
|
||||
|
||||
- name: 🛠️ Generate project files
|
||||
run: bun run prebuild
|
||||
|
||||
- name: 🏗️ Setup EAS
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
eas-cache: true
|
||||
|
||||
- name: ⚙️ Ensure iOS SDKs installed
|
||||
run: xcodebuild -downloadPlatform iOS
|
||||
|
||||
- name: 🚀 Build iOS app
|
||||
env:
|
||||
EXPO_TV: 0
|
||||
run: eas build -p ios --local --non-interactive
|
||||
|
||||
- name: 📅 Set date tag
|
||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: 📤 Upload IPA artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: streamyfin-ios-phone-ipa-${{ env.DATE_TAG }}
|
||||
path: build-*.ipa
|
||||
retention-days: 7
|
||||
|
||||
# Disabled for now - uncomment when ready to build iOS TV
|
||||
# build-ios-tv:
|
||||
# if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
|
||||
# runs-on: macos-15
|
||||
# name: 🍎 Build iOS IPA (TV)
|
||||
# permissions:
|
||||
# contents: read
|
||||
#
|
||||
# steps:
|
||||
# - name: 📥 Checkout code
|
||||
# uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
# with:
|
||||
# ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
# fetch-depth: 0
|
||||
# submodules: recursive
|
||||
# show-progress: false
|
||||
#
|
||||
# - name: 🍞 Setup Bun
|
||||
# uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
# with:
|
||||
# bun-version: latest
|
||||
#
|
||||
# - name: 💾 Cache Bun dependencies
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
# with:
|
||||
# path: ~/.bun/install/cache
|
||||
# key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-bun-cache
|
||||
#
|
||||
# - name: 📦 Install dependencies and reload submodules
|
||||
# run: |
|
||||
# bun install --frozen-lockfile
|
||||
# bun run submodule-reload
|
||||
#
|
||||
# - name: 🛠️ Generate project files
|
||||
# run: bun run prebuild:tv
|
||||
#
|
||||
# - name: 🏗️ Setup EAS
|
||||
# uses: expo/expo-github-action@main
|
||||
# with:
|
||||
# eas-version: latest
|
||||
# token: ${{ secrets.EXPO_TOKEN }}
|
||||
# eas-cache: true
|
||||
#
|
||||
# - name: ⚙️ Ensure tvOS SDKs installed
|
||||
# run: xcodebuild -downloadPlatform tvOS
|
||||
#
|
||||
# - name: 🚀 Build iOS app
|
||||
# env:
|
||||
# EXPO_TV: 1
|
||||
# run: eas build -p ios --local --non-interactive
|
||||
#
|
||||
# - name: 📅 Set date tag
|
||||
# run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||
#
|
||||
# - name: 📤 Upload IPA artifact
|
||||
# uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
# with:
|
||||
# name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }}
|
||||
# path: build-*.ipa
|
||||
# retention-days: 7
|
||||
95
.github/workflows/build-ios.yml
vendored
95
.github/workflows/build-ios.yml
vendored
@@ -1,95 +0,0 @@
|
||||
name: 🤖 iOS IPA Build (Phone + TV)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [develop, master]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
push:
|
||||
branches: [develop, master]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
|
||||
jobs:
|
||||
build-ios:
|
||||
if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin'))
|
||||
runs-on: macos-15
|
||||
name: 🏗️ Build iOS IPA
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [phone]
|
||||
# target: [phone, tv]
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
show-progress: false
|
||||
|
||||
- name: 🍞 Setup Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 💾 Cache Bun dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-cache
|
||||
|
||||
- name: 📦 Install dependencies and reload submodules
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run submodule-reload
|
||||
|
||||
- name: 🛠️ Generate project files
|
||||
run: |
|
||||
if [ "${{ matrix.target }}" = "tv" ]; then
|
||||
bun run prebuild:tv
|
||||
else
|
||||
bun run prebuild
|
||||
fi
|
||||
|
||||
- name: 🏗️ Setup EAS
|
||||
uses: expo/expo-github-action@main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
eas-cache: true
|
||||
|
||||
- name: ⚙️ Ensure iOS/tvOS SDKs installed
|
||||
run: |
|
||||
if [ "${{ matrix.target }}" = "tv" ]; then
|
||||
xcodebuild -downloadPlatform tvOS
|
||||
else
|
||||
xcodebuild -downloadPlatform iOS
|
||||
fi
|
||||
|
||||
- name: 🚀 Build iOS app
|
||||
env:
|
||||
EXPO_TV: ${{ matrix.target == 'tv' && 1 || 0 }}
|
||||
run: eas build -p ios --local --non-interactive
|
||||
|
||||
- name: 📅 Set date tag
|
||||
run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: 📤 Upload IPA artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: streamyfin-ios-${{ matrix.target }}-ipa-${{ env.DATE_TAG }}
|
||||
path: build-*.ipa
|
||||
retention-days: 7
|
||||
34
.github/workflows/crowdin.yml
vendored
Normal file
34
.github/workflows/crowdin.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Crowdin Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
localization_branch_name: l10n_crowdin_translations
|
||||
create_pull_request: true
|
||||
pull_request_title: 'feat: New Crowdin Translations'
|
||||
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
||||
pull_request_base_branch_name: 'develop'
|
||||
env:
|
||||
# A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository).
|
||||
GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
|
||||
|
||||
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
|
||||
# Visit https://crowdin.com/settings#api-key to create this token
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
4
app.json
4
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "Streamyfin",
|
||||
"slug": "streamyfin",
|
||||
"version": "0.38.0",
|
||||
"version": "0.39.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "streamyfin",
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"android": {
|
||||
"jsEngine": "hermes",
|
||||
"versionCode": 70,
|
||||
"versionCode": 71,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/icon-android-plain.png",
|
||||
"monochromeImage": "./assets/images/icon-android-themed.png",
|
||||
|
||||
@@ -93,6 +93,19 @@ export default function page() {
|
||||
}
|
||||
}, [downloadedFiles]);
|
||||
|
||||
const otherMedia = useMemo(() => {
|
||||
try {
|
||||
return (
|
||||
downloadedFiles?.filter(
|
||||
(f) => f.item.Type !== "Movie" && f.item.Type !== "Episode",
|
||||
) || []
|
||||
);
|
||||
} catch {
|
||||
setShowMigration(true);
|
||||
return [];
|
||||
}
|
||||
}, [downloadedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
@@ -131,8 +144,30 @@ export default function page() {
|
||||
writeToLog("ERROR", reason);
|
||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
|
||||
});
|
||||
const deleteOtherMedia = () =>
|
||||
Promise.all(
|
||||
otherMedia.map((item) =>
|
||||
deleteFileByType(item.item.Type)
|
||||
.then(() =>
|
||||
toast.success(
|
||||
t("home.downloads.toasts.deleted_media_successfully", {
|
||||
type: item.item.Type,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch((reason) => {
|
||||
writeToLog("ERROR", reason);
|
||||
toast.error(
|
||||
t("home.downloads.toasts.failed_to_delete_media", {
|
||||
type: item.item.Type,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const deleteAllMedia = async () =>
|
||||
await Promise.all([deleteMovies(), deleteShows()]);
|
||||
await Promise.all([deleteMovies(), deleteShows(), deleteOtherMedia()]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -241,6 +276,34 @@ export default function page() {
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{otherMedia.length > 0 && (
|
||||
<View className='mb-4'>
|
||||
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
||||
<Text className='text-lg font-bold'>
|
||||
{t("home.downloads.other_media")}
|
||||
</Text>
|
||||
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
||||
<Text className='text-xs font-bold'>
|
||||
{otherMedia?.length}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View className='px-4 flex flex-row'>
|
||||
{otherMedia?.map((item) => (
|
||||
<TouchableItemRouter
|
||||
item={item.item}
|
||||
isOffline
|
||||
key={item.item.Id}
|
||||
>
|
||||
<MovieCard item={item.item} />
|
||||
</TouchableItemRouter>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{downloadedFiles?.length === 0 && (
|
||||
<View className='flex px-4'>
|
||||
<Text className='opacity-50'>
|
||||
@@ -276,6 +339,11 @@ export default function page() {
|
||||
<Button color='purple' onPress={deleteShows}>
|
||||
{t("home.downloads.delete_all_tvseries_button")}
|
||||
</Button>
|
||||
{otherMedia.length > 0 && (
|
||||
<Button color='purple' onPress={deleteOtherMedia}>
|
||||
{t("home.downloads.delete_all_other_media_button")}
|
||||
</Button>
|
||||
)}
|
||||
<Button color='red' onPress={deleteAllMedia}>
|
||||
{t("home.downloads.delete_all_button")}
|
||||
</Button>
|
||||
|
||||
@@ -23,112 +23,117 @@ export default function IndexLayout() {
|
||||
headerBlurEffect: "none",
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
headerRight: () => (
|
||||
<PlatformDropdown
|
||||
trigger={
|
||||
<Ionicons
|
||||
name='ellipsis-horizontal-outline'
|
||||
size={24}
|
||||
color='white'
|
||||
/>
|
||||
}
|
||||
title={t("library.options.display")}
|
||||
groups={[
|
||||
{
|
||||
title: t("library.options.display"),
|
||||
options: [
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.row"),
|
||||
value: "row",
|
||||
selected: settings.libraryOptions.display === "row",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
display: "row",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.list"),
|
||||
value: "list",
|
||||
selected: settings.libraryOptions.display === "list",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
display: "list",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("library.options.image_style"),
|
||||
options: [
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.poster"),
|
||||
value: "poster",
|
||||
selected: settings.libraryOptions.imageStyle === "poster",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
imageStyle: "poster",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.cover"),
|
||||
value: "cover",
|
||||
selected: settings.libraryOptions.imageStyle === "cover",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
imageStyle: "cover",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Options",
|
||||
options: [
|
||||
{
|
||||
type: "toggle",
|
||||
label: t("library.options.show_titles"),
|
||||
value: settings.libraryOptions.showTitles,
|
||||
onToggle: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
showTitles: !settings.libraryOptions.showTitles,
|
||||
},
|
||||
}),
|
||||
disabled: settings.libraryOptions.imageStyle === "poster",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
label: t("library.options.show_stats"),
|
||||
value: settings.libraryOptions.showStats,
|
||||
onToggle: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
showStats: !settings.libraryOptions.showStats,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
headerRight: () =>
|
||||
!pluginSettings?.libraryOptions?.locked &&
|
||||
!Platform.isTV && (
|
||||
<PlatformDropdown
|
||||
trigger={
|
||||
<Ionicons
|
||||
name='ellipsis-horizontal-outline'
|
||||
size={24}
|
||||
color='white'
|
||||
/>
|
||||
}
|
||||
title={t("library.options.display")}
|
||||
groups={[
|
||||
{
|
||||
title: t("library.options.display"),
|
||||
options: [
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.row"),
|
||||
value: "row",
|
||||
selected: settings.libraryOptions.display === "row",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
display: "row",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.list"),
|
||||
value: "list",
|
||||
selected: settings.libraryOptions.display === "list",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
display: "list",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("library.options.image_style"),
|
||||
options: [
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.poster"),
|
||||
value: "poster",
|
||||
selected:
|
||||
settings.libraryOptions.imageStyle === "poster",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
imageStyle: "poster",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
label: t("library.options.cover"),
|
||||
value: "cover",
|
||||
selected:
|
||||
settings.libraryOptions.imageStyle === "cover",
|
||||
onPress: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
imageStyle: "cover",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Options",
|
||||
options: [
|
||||
{
|
||||
type: "toggle",
|
||||
label: t("library.options.show_titles"),
|
||||
value: settings.libraryOptions.showTitles,
|
||||
onToggle: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
showTitles: !settings.libraryOptions.showTitles,
|
||||
},
|
||||
}),
|
||||
disabled:
|
||||
settings.libraryOptions.imageStyle === "poster",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
label: t("library.options.show_stats"),
|
||||
value: settings.libraryOptions.showStats,
|
||||
onToggle: () =>
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
showStats: !settings.libraryOptions.showStats,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
|
||||
@@ -22,6 +22,12 @@ import { BITRATES } from "@/components/BitrateSelector";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { Controls } from "@/components/video-player/controls/Controls";
|
||||
import {
|
||||
OUTLINE_THICKNESS,
|
||||
OutlineThickness,
|
||||
VLC_COLORS,
|
||||
VLCColor,
|
||||
} from "@/constants/SubtitleConstants";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||
@@ -101,7 +107,7 @@ export default function page() {
|
||||
/** Playback position in ticks. */
|
||||
playbackPosition?: string;
|
||||
}>();
|
||||
useSettings();
|
||||
const { settings } = useSettings();
|
||||
|
||||
const offline = offlineStr === "true";
|
||||
const playbackManager = usePlaybackManager();
|
||||
@@ -560,8 +566,34 @@ export default function page() {
|
||||
? allSubs.indexOf(chosenSubtitleTrack)
|
||||
: [...textSubs].reverse().indexOf(chosenSubtitleTrack);
|
||||
initOptions.push(`--sub-track=${finalIndex}`);
|
||||
}
|
||||
|
||||
// Add VLC subtitle styling options from settings
|
||||
const textColor = (settings.vlcTextColor ?? "White") as VLCColor;
|
||||
const backgroundColor = (settings.vlcBackgroundColor ??
|
||||
"Black") as VLCColor;
|
||||
const outlineColor = (settings.vlcOutlineColor ?? "Black") as VLCColor;
|
||||
const outlineThickness = (settings.vlcOutlineThickness ??
|
||||
"Normal") as OutlineThickness;
|
||||
const backgroundOpacity = settings.vlcBackgroundOpacity ?? 128;
|
||||
const outlineOpacity = settings.vlcOutlineOpacity ?? 255;
|
||||
const isBold = settings.vlcIsBold ?? false;
|
||||
// Add subtitle styling options
|
||||
initOptions.push(`--freetype-color=${VLC_COLORS[textColor]}`);
|
||||
initOptions.push(`--freetype-background-opacity=${backgroundOpacity}`);
|
||||
initOptions.push(
|
||||
`--freetype-background-color=${VLC_COLORS[backgroundColor]}`,
|
||||
);
|
||||
initOptions.push(`--freetype-outline-opacity=${outlineOpacity}`);
|
||||
initOptions.push(`--freetype-outline-color=${VLC_COLORS[outlineColor]}`);
|
||||
initOptions.push(
|
||||
`--freetype-outline-thickness=${OUTLINE_THICKNESS[outlineThickness]}`,
|
||||
);
|
||||
initOptions.push(`--sub-text-scale=${settings.subtitleSize}`);
|
||||
initOptions.push("--sub-margin=40");
|
||||
if (isBold) {
|
||||
initOptions.push("--freetype-bold");
|
||||
}
|
||||
}
|
||||
if (notTranscoding && chosenAudioTrack) {
|
||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
||||
*/
|
||||
const handleDeleteFile = useCallback(() => {
|
||||
if (item.Id) {
|
||||
deleteFile(item.Id, "Movie");
|
||||
deleteFile(item.Id, item.Type);
|
||||
}
|
||||
}, [deleteFile, item.Id]);
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ export const StorageSettings = () => {
|
||||
};
|
||||
|
||||
const calculatePercentage = (value: number, total: number) => {
|
||||
console.log("usage", value, total);
|
||||
return ((value / total) * 100).toFixed(2);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@ import { useTranslation } from "react-i18next";
|
||||
import { Platform, View, type ViewProps } from "react-native";
|
||||
import { Switch } from "react-native-gesture-handler";
|
||||
import { Stepper } from "@/components/inputs/Stepper";
|
||||
import {
|
||||
OUTLINE_THICKNESS,
|
||||
type OutlineThickness,
|
||||
VLC_COLORS,
|
||||
type VLCColor,
|
||||
} from "@/constants/SubtitleConstants";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { Text } from "../common/Text";
|
||||
import { ListGroup } from "../list/ListGroup";
|
||||
@@ -86,6 +92,84 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
||||
];
|
||||
}, [settings?.subtitleMode, t, updateSettings]);
|
||||
|
||||
const textColorOptionGroups = useMemo(() => {
|
||||
const colors = Object.keys(VLC_COLORS) as VLCColor[];
|
||||
const options = colors.map((color) => ({
|
||||
type: "radio" as const,
|
||||
label: t(`home.settings.subtitles.colors.${color}`),
|
||||
value: color,
|
||||
selected: (settings?.vlcTextColor || "White") === color,
|
||||
onPress: () => updateSettings({ vlcTextColor: color }),
|
||||
}));
|
||||
|
||||
return [{ options }];
|
||||
}, [settings?.vlcTextColor, t, updateSettings]);
|
||||
|
||||
const backgroundColorOptionGroups = useMemo(() => {
|
||||
const colors = Object.keys(VLC_COLORS) as VLCColor[];
|
||||
const options = colors.map((color) => ({
|
||||
type: "radio" as const,
|
||||
label: t(`home.settings.subtitles.colors.${color}`),
|
||||
value: color,
|
||||
selected: (settings?.vlcBackgroundColor || "Black") === color,
|
||||
onPress: () => updateSettings({ vlcBackgroundColor: color }),
|
||||
}));
|
||||
|
||||
return [{ options }];
|
||||
}, [settings?.vlcBackgroundColor, t, updateSettings]);
|
||||
|
||||
const outlineColorOptionGroups = useMemo(() => {
|
||||
const colors = Object.keys(VLC_COLORS) as VLCColor[];
|
||||
const options = colors.map((color) => ({
|
||||
type: "radio" as const,
|
||||
label: t(`home.settings.subtitles.colors.${color}`),
|
||||
value: color,
|
||||
selected: (settings?.vlcOutlineColor || "Black") === color,
|
||||
onPress: () => updateSettings({ vlcOutlineColor: color }),
|
||||
}));
|
||||
|
||||
return [{ options }];
|
||||
}, [settings?.vlcOutlineColor, t, updateSettings]);
|
||||
|
||||
const outlineThicknessOptionGroups = useMemo(() => {
|
||||
const thicknesses = Object.keys(OUTLINE_THICKNESS) as OutlineThickness[];
|
||||
const options = thicknesses.map((thickness) => ({
|
||||
type: "radio" as const,
|
||||
label: t(`home.settings.subtitles.thickness.${thickness}`),
|
||||
value: thickness,
|
||||
selected: (settings?.vlcOutlineThickness || "Normal") === thickness,
|
||||
onPress: () => updateSettings({ vlcOutlineThickness: thickness }),
|
||||
}));
|
||||
|
||||
return [{ options }];
|
||||
}, [settings?.vlcOutlineThickness, t, updateSettings]);
|
||||
|
||||
const backgroundOpacityOptionGroups = useMemo(() => {
|
||||
const opacities = [0, 32, 64, 96, 128, 160, 192, 224, 255];
|
||||
const options = opacities.map((opacity) => ({
|
||||
type: "radio" as const,
|
||||
label: `${Math.round((opacity / 255) * 100)}%`,
|
||||
value: opacity,
|
||||
selected: (settings?.vlcBackgroundOpacity ?? 128) === opacity,
|
||||
onPress: () => updateSettings({ vlcBackgroundOpacity: opacity }),
|
||||
}));
|
||||
|
||||
return [{ options }];
|
||||
}, [settings?.vlcBackgroundOpacity, updateSettings]);
|
||||
|
||||
const outlineOpacityOptionGroups = useMemo(() => {
|
||||
const opacities = [0, 32, 64, 96, 128, 160, 192, 224, 255];
|
||||
const options = opacities.map((opacity) => ({
|
||||
type: "radio" as const,
|
||||
label: `${Math.round((opacity / 255) * 100)}%`,
|
||||
value: opacity,
|
||||
selected: (settings?.vlcOutlineOpacity ?? 255) === opacity,
|
||||
onPress: () => updateSettings({ vlcOutlineOpacity: opacity }),
|
||||
}));
|
||||
|
||||
return [{ options }];
|
||||
}, [settings?.vlcOutlineOpacity, updateSettings]);
|
||||
|
||||
if (isTv) return null;
|
||||
if (!settings) return null;
|
||||
|
||||
@@ -168,6 +252,124 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
|
||||
onUpdate={(subtitleSize) => updateSettings({ subtitleSize })}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.subtitles.text_color")}>
|
||||
<PlatformDropdown
|
||||
groups={textColorOptionGroups}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>
|
||||
{t(
|
||||
`home.settings.subtitles.colors.${settings?.vlcTextColor || "White"}`,
|
||||
)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.subtitles.text_color")}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.subtitles.background_color")}>
|
||||
<PlatformDropdown
|
||||
groups={backgroundColorOptionGroups}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>
|
||||
{t(
|
||||
`home.settings.subtitles.colors.${settings?.vlcBackgroundColor || "Black"}`,
|
||||
)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.subtitles.background_color")}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.subtitles.outline_color")}>
|
||||
<PlatformDropdown
|
||||
groups={outlineColorOptionGroups}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>
|
||||
{t(
|
||||
`home.settings.subtitles.colors.${settings?.vlcOutlineColor || "Black"}`,
|
||||
)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.subtitles.outline_color")}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.subtitles.outline_thickness")}>
|
||||
<PlatformDropdown
|
||||
groups={outlineThicknessOptionGroups}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>
|
||||
{t(
|
||||
`home.settings.subtitles.thickness.${settings?.vlcOutlineThickness || "Normal"}`,
|
||||
)}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.subtitles.outline_thickness")}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.subtitles.background_opacity")}>
|
||||
<PlatformDropdown
|
||||
groups={backgroundOpacityOptionGroups}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>{`${Math.round(((settings?.vlcBackgroundOpacity ?? 128) / 255) * 100)}%`}</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.subtitles.background_opacity")}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.subtitles.outline_opacity")}>
|
||||
<PlatformDropdown
|
||||
groups={outlineOpacityOptionGroups}
|
||||
trigger={
|
||||
<View className='flex flex-row items-center justify-between py-3 pl-3'>
|
||||
<Text className='mr-1 text-[#8E8D91]'>{`${Math.round(((settings?.vlcOutlineOpacity ?? 255) / 255) * 100)}%`}</Text>
|
||||
<Ionicons
|
||||
name='chevron-expand-sharp'
|
||||
size={18}
|
||||
color='#5A5960'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
title={t("home.settings.subtitles.outline_opacity")}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.subtitles.bold_text")}>
|
||||
<Switch
|
||||
value={settings?.vlcIsBold ?? false}
|
||||
onValueChange={(value) => updateSettings({ vlcIsBold: value })}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListGroup>
|
||||
</View>
|
||||
);
|
||||
|
||||
45
constants/SubtitleConstants.ts
Normal file
45
constants/SubtitleConstants.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export type VLCColor =
|
||||
| "Black"
|
||||
| "Gray"
|
||||
| "Silver"
|
||||
| "White"
|
||||
| "Maroon"
|
||||
| "Red"
|
||||
| "Fuchsia"
|
||||
| "Yellow"
|
||||
| "Olive"
|
||||
| "Green"
|
||||
| "Teal"
|
||||
| "Lime"
|
||||
| "Purple"
|
||||
| "Navy"
|
||||
| "Blue"
|
||||
| "Aqua";
|
||||
|
||||
export type OutlineThickness = "None" | "Thin" | "Normal" | "Thick";
|
||||
|
||||
export const VLC_COLORS: Record<VLCColor, number> = {
|
||||
Black: 0,
|
||||
Gray: 8421504,
|
||||
Silver: 12632256,
|
||||
White: 16777215,
|
||||
Maroon: 8388608,
|
||||
Red: 16711680,
|
||||
Fuchsia: 16711935,
|
||||
Yellow: 16776960,
|
||||
Olive: 8421376,
|
||||
Green: 32768,
|
||||
Teal: 32896,
|
||||
Lime: 65280,
|
||||
Purple: 8388736,
|
||||
Navy: 128,
|
||||
Blue: 255,
|
||||
Aqua: 65535,
|
||||
};
|
||||
|
||||
export const OUTLINE_THICKNESS: Record<OutlineThickness, number> = {
|
||||
None: 0,
|
||||
Thin: 2,
|
||||
Normal: 4,
|
||||
Thick: 6,
|
||||
};
|
||||
12
crowdin.yml
Normal file
12
crowdin.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
"project_id_env": "CROWDIN_PROJECT_ID"
|
||||
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
|
||||
"base_path": "."
|
||||
|
||||
"preserve_hierarchy": true
|
||||
|
||||
"files": [
|
||||
{
|
||||
"source": "translations/en.json",
|
||||
"translation": "translations/%two_letters_code%.json"
|
||||
}
|
||||
]
|
||||
6
eas.json
6
eas.json
@@ -45,14 +45,14 @@
|
||||
},
|
||||
"production": {
|
||||
"environment": "production",
|
||||
"channel": "0.38.0",
|
||||
"channel": "0.39.0",
|
||||
"android": {
|
||||
"image": "latest"
|
||||
}
|
||||
},
|
||||
"production-apk": {
|
||||
"environment": "production",
|
||||
"channel": "0.38.0",
|
||||
"channel": "0.39.0",
|
||||
"android": {
|
||||
"buildType": "apk",
|
||||
"image": "latest"
|
||||
@@ -60,7 +60,7 @@
|
||||
},
|
||||
"production-apk-tv": {
|
||||
"environment": "production",
|
||||
"channel": "0.38.0",
|
||||
"channel": "0.39.0",
|
||||
"android": {
|
||||
"buildType": "apk",
|
||||
"image": "latest"
|
||||
|
||||
@@ -395,7 +395,7 @@ function useDownloadProvider() {
|
||||
return db.movies[id];
|
||||
}
|
||||
|
||||
// If not in movies, check episodes
|
||||
// Check episodes
|
||||
for (const series of Object.values(db.series)) {
|
||||
for (const season of Object.values(series.seasons)) {
|
||||
for (const episode of Object.values(season.episodes)) {
|
||||
@@ -408,6 +408,11 @@ function useDownloadProvider() {
|
||||
}
|
||||
|
||||
console.log(`[DB] No item found with ID: ${id}`);
|
||||
// Check other media types
|
||||
if (db.other[id]) {
|
||||
return db.other[id];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -448,7 +453,7 @@ function useDownloadProvider() {
|
||||
const db = JSON.parse(file) as DownloadsDatabase;
|
||||
return db;
|
||||
}
|
||||
return { movies: {}, series: {} };
|
||||
return { movies: {}, series: {}, other: {} }; // Initialize other media types storage
|
||||
};
|
||||
|
||||
const getDownloadedItems = useCallback(() => {
|
||||
@@ -459,23 +464,8 @@ function useDownloadProvider() {
|
||||
Object.values(season.episodes),
|
||||
),
|
||||
);
|
||||
const allItems = [...movies, ...episodes];
|
||||
|
||||
// Only log when there are items to avoid spam
|
||||
if (allItems.length > 0) {
|
||||
console.log(
|
||||
`[DB] Retrieved ${movies.length} movies and ${episodes.length} episodes from database`,
|
||||
);
|
||||
console.log(`[DB] Total downloaded items: ${allItems.length}`);
|
||||
|
||||
// Log details of each item for debugging
|
||||
allItems.forEach((item, index) => {
|
||||
console.log(
|
||||
`[DB] Item ${index + 1}: ${item.item.Name} - Path: ${item.videoFilePath}, Size: ${item.videoFileSize}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const otherItems = Object.values(db.other);
|
||||
const allItems = [...movies, ...episodes, ...otherItems];
|
||||
return allItems;
|
||||
}, []);
|
||||
|
||||
@@ -777,6 +767,9 @@ function useDownloadProvider() {
|
||||
db.series[item.SeriesId].seasons[seasonNumber].episodes[
|
||||
episodeNumber
|
||||
] = downloadedItem;
|
||||
} else if (item.Id) {
|
||||
// Handle other media types
|
||||
db.other[item.Id] = downloadedItem;
|
||||
}
|
||||
|
||||
await saveDownloadsDatabase(db);
|
||||
@@ -947,16 +940,16 @@ function useDownloadProvider() {
|
||||
[authHeader, startDownload],
|
||||
);
|
||||
|
||||
const deleteFile = async (id: string, type: "Movie" | "Episode") => {
|
||||
const deleteFile = async (id: string, type: BaseItemDto["Type"]) => {
|
||||
const db = getDownloadsDatabase();
|
||||
let downloadedItem: DownloadedItem | undefined;
|
||||
|
||||
if (type === "Movie") {
|
||||
if (type === "Movie" && Object.entries(db.movies).length !== 0) {
|
||||
downloadedItem = db.movies[id];
|
||||
if (downloadedItem) {
|
||||
delete db.movies[id];
|
||||
}
|
||||
} else if (type === "Episode") {
|
||||
} else if (type === "Episode" && Object.entries(db.series).length !== 0) {
|
||||
const cleanUpEmptyParents = (
|
||||
series: any,
|
||||
seasonNumber: string,
|
||||
@@ -986,6 +979,12 @@ function useDownloadProvider() {
|
||||
}
|
||||
if (downloadedItem) break;
|
||||
}
|
||||
} else {
|
||||
// Handle other media types
|
||||
downloadedItem = db.other[id];
|
||||
if (downloadedItem) {
|
||||
delete db.other[id];
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedItem?.videoFilePath) {
|
||||
@@ -1091,7 +1090,7 @@ function useDownloadProvider() {
|
||||
|
||||
const deleteItems = async (items: BaseItemDto[]) => {
|
||||
for (const item of items) {
|
||||
if (item.Id && (item.Type === "Movie" || item.Type === "Episode")) {
|
||||
if (item.Id) {
|
||||
await deleteFile(item.Id, item.Type);
|
||||
}
|
||||
}
|
||||
@@ -1134,6 +1133,8 @@ function useDownloadProvider() {
|
||||
const db = getDownloadsDatabase();
|
||||
if (db.movies[itemId]) {
|
||||
db.movies[itemId] = updatedItem;
|
||||
} else if (db.other[itemId]) {
|
||||
db.other[itemId] = updatedItem;
|
||||
} else {
|
||||
for (const series of Object.values(db.series)) {
|
||||
for (const season of Object.values(series.seasons)) {
|
||||
|
||||
@@ -90,6 +90,8 @@ export interface DownloadsDatabase {
|
||||
movies: Record<string, DownloadedItem>;
|
||||
/** A map of series IDs to their downloaded series data. */
|
||||
series: Record<string, DownloadedSeries>;
|
||||
/** A map of IDs to downloaded items that are neither movies nor episodes */
|
||||
other: Record<string, DownloadedItem>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setJellyfin(
|
||||
() =>
|
||||
new Jellyfin({
|
||||
clientInfo: { name: "Streamyfin", version: "0.38.0" },
|
||||
clientInfo: { name: "Streamyfin", version: "0.39.0" },
|
||||
deviceInfo: {
|
||||
name: deviceName,
|
||||
id,
|
||||
@@ -87,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
return {
|
||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||
Platform.OS === "android" ? "Android" : "iOS"
|
||||
}, DeviceId="${deviceId}", Version="0.38.0"`,
|
||||
}, DeviceId="${deviceId}", Version="0.39.0"`,
|
||||
};
|
||||
}, [deviceId]);
|
||||
|
||||
|
||||
@@ -111,11 +111,11 @@
|
||||
},
|
||||
"subtitles": {
|
||||
"subtitle_title": "Subtitles",
|
||||
"subtitle_language": "Subtitle Language",
|
||||
"subtitle_hint": "Configure how subtitles look and behave.",
|
||||
"subtitle_language": "Subtitle language",
|
||||
"subtitle_mode": "Subtitle Mode",
|
||||
"set_subtitle_track": "Set Subtitle Track From Previous Item",
|
||||
"subtitle_size": "Subtitle Size",
|
||||
"subtitle_hint": "Configure Subtitle Preference.",
|
||||
"none": "None",
|
||||
"language": "Language",
|
||||
"loading": "Loading",
|
||||
@@ -124,7 +124,38 @@
|
||||
"Smart": "Smart",
|
||||
"Always": "Always",
|
||||
"None": "None",
|
||||
"OnlyForced": "Only Forced"
|
||||
"OnlyForced": "OnlyForced"
|
||||
},
|
||||
"text_color": "Text Color",
|
||||
"background_color": "Background Color",
|
||||
"outline_color": "Outline Color",
|
||||
"outline_thickness": "Outline Thickness",
|
||||
"background_opacity": "Background Opacity",
|
||||
"outline_opacity": "Outline Opacity",
|
||||
"bold_text": "Bold Text",
|
||||
"colors": {
|
||||
"Black": "Black",
|
||||
"Gray": "Gray",
|
||||
"Silver": "Silver",
|
||||
"White": "White",
|
||||
"Maroon": "Maroon",
|
||||
"Red": "Red",
|
||||
"Fuchsia": "Fuchsia",
|
||||
"Yellow": "Yellow",
|
||||
"Olive": "Olive",
|
||||
"Green": "Green",
|
||||
"Teal": "Teal",
|
||||
"Lime": "Lime",
|
||||
"Purple": "Purple",
|
||||
"Navy": "Navy",
|
||||
"Blue": "Blue",
|
||||
"Aqua": "Aqua"
|
||||
},
|
||||
"thickness": {
|
||||
"None": "None",
|
||||
"Thin": "Thin",
|
||||
"Normal": "Normal",
|
||||
"Thick": "Thick"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
@@ -237,12 +268,14 @@
|
||||
"tvseries": "TV-Series",
|
||||
"movies": "Movies",
|
||||
"queue": "Queue",
|
||||
"other_media": "Other media",
|
||||
"queue_hint": "Queue and downloads will be lost on app restart",
|
||||
"no_items_in_queue": "No Items in Queue",
|
||||
"no_downloaded_items": "No Downloaded Items",
|
||||
"delete_all_movies_button": "Delete All Movies",
|
||||
"delete_all_tvseries_button": "Delete All TV-Series",
|
||||
"delete_all_button": "Delete All",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Active Download",
|
||||
"no_active_downloads": "No Active Downloads",
|
||||
"active_downloads": "Active Downloads",
|
||||
@@ -259,6 +292,8 @@
|
||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
"could_not_delete_download": "Could Not Delete Download",
|
||||
"download_paused": "Download Paused",
|
||||
|
||||
464
translations/hu.json
Normal file
464
translations/hu.json
Normal file
@@ -0,0 +1,464 @@
|
||||
{
|
||||
"login": {
|
||||
"username_required": "A felhasználónév megadása kötelező",
|
||||
"error_title": "Hiba",
|
||||
"login_title": "Bejelentkezés",
|
||||
"login_to_title": "Bejelentkezés ide",
|
||||
"username_placeholder": "Felhasználónév",
|
||||
"password_placeholder": "Jelszó",
|
||||
"login_button": "Bejelentkezés",
|
||||
"quick_connect": "Gyorscsatlakozás",
|
||||
"enter_code_to_login": "Írd be a {{code}} kódot a bejelentkezéshez",
|
||||
"failed_to_initiate_quick_connect": "A Gyorscsatlakozás kezdeményezése sikertelen.",
|
||||
"got_it": "Értettem",
|
||||
"connection_failed": "Kapcsolódás Sikertelen",
|
||||
"could_not_connect_to_server": "Nem sikerült csatlakozni a szerverhez. Kérjük, ellenőrizd az URL-t és a hálózati kapcsolatot.",
|
||||
"an_unexpected_error_occured": "Váratlan Hiba Történt",
|
||||
"change_server": "Szerverváltás",
|
||||
"invalid_username_or_password": "Érvénytelen Felhasználónév vagy Jelszó",
|
||||
"user_does_not_have_permission_to_log_in": "A felhasználónak nincs jogosultsága a bejelentkezéshez",
|
||||
"server_is_taking_too_long_to_respond_try_again_later": "A szerver túl sokáig válaszol, próbáld újra később",
|
||||
"server_received_too_many_requests_try_again_later": "A szerver túl sok kérést kapott, próbáld újra később.",
|
||||
"there_is_a_server_error": "Szerverhiba történt",
|
||||
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Váratlan hiba történt. Helyesen adtad meg a szerver URL-jét?",
|
||||
"too_old_server_text": "Nem Támogatott Jellyfin-szerver",
|
||||
"too_old_server_description": "Frissítsd a Jellyfint a legújabb verzióra"
|
||||
},
|
||||
"server": {
|
||||
"enter_url_to_jellyfin_server": "Add meg a Jellyfin szerver URL-jét",
|
||||
"server_url_placeholder": "http(s)://a-te-szervered.hu",
|
||||
"connect_button": "Csatlakozás",
|
||||
"previous_servers": "Előző Szerverek",
|
||||
"clear_button": "Törlés",
|
||||
"search_for_local_servers": "Helyi Szerverek Keresése",
|
||||
"searching": "Keresés...",
|
||||
"servers": "Szerverek"
|
||||
},
|
||||
"home": {
|
||||
"checking_server_connection": "Szerverkapcsolat ellenőrzése...",
|
||||
"no_internet": "Nincs Internet",
|
||||
"no_items": "Nincsenek elemek",
|
||||
"no_internet_message": "Semmi gond, továbbra is nézheted\na letöltött tartalmakat.",
|
||||
"checking_server_connection_message": "Kapcsolat ellenőrzése a szerverrel",
|
||||
"go_to_downloads": "Ugrás a Letöltésekhez",
|
||||
"retry": "Újra",
|
||||
"server_unreachable": "Szerver Elérhetetlen",
|
||||
"server_unreachable_message": "Nem sikerült elérni a szervert.\nKérjük, ellenőrizd a hálózati kapcsolatot.",
|
||||
"oops": "Hoppá!",
|
||||
"error_message": "Valami nem stimmel.\nKérjük, jelentkezz ki, majd újra be.",
|
||||
"continue_watching": "Nézd Tovább",
|
||||
"next_up": "Következő",
|
||||
"recently_added_in": "Új a(z) {{libraryName}} könyvtárban",
|
||||
"suggested_movies": "Javasolt Filmek",
|
||||
"suggested_episodes": "Javasolt Epizódok",
|
||||
"intro": {
|
||||
"welcome_to_streamyfin": "Üdvözöljük a Streamyfinben",
|
||||
"a_free_and_open_source_client_for_jellyfin": "Egy Ingyenes és Nyílt Forráskódú Jellyfin Kliens.",
|
||||
"features_title": "Funkciók",
|
||||
"features_description": "A Streamyfin számos funkcióval rendelkezik és sokféle szoftverrel integrálható, melyeket a beállítások menüben találhatsz:",
|
||||
"jellyseerr_feature_description": "Csatlakozz a Jellyseerrhez és kérj filmeket közvetlenül az alkalmazásból.",
|
||||
"downloads_feature_title": "Letöltések",
|
||||
"downloads_feature_description": "Töltsd le a filmeket és sorozatokat az offline megtekintéshez. Használhatod az alapértelmezett módszert, vagy telepítheted az 'optimise server'-t a háttérben történő letöltéshez.",
|
||||
"chromecast_feature_description": "Játszd le a filmeket és sorozatokat a Chromecast eszközeiden.",
|
||||
"centralised_settings_plugin_title": "Központosított Beállítások Bővítmény",
|
||||
"centralised_settings_plugin_description": "Konfiguráld a beállításaidat központilag a Jellyfin szerveren. Minden felhasználói kliensbeállítás automatikusan szinkronizálódik.",
|
||||
"done_button": "Kész",
|
||||
"go_to_settings_button": "Ugrás a Beállításokhoz",
|
||||
"read_more": "Bővebben"
|
||||
},
|
||||
"settings": {
|
||||
"settings_title": "Beállítások",
|
||||
"log_out_button": "Kijelentkezés",
|
||||
"user_info": {
|
||||
"user_info_title": "Felhasználói Információk",
|
||||
"user": "Felhasználó",
|
||||
"server": "Szerver",
|
||||
"token": "Token",
|
||||
"app_version": "Alkalmazásverzió"
|
||||
},
|
||||
"quick_connect": {
|
||||
"quick_connect_title": "Gyorscsatlakozás",
|
||||
"authorize_button": "Gyorscsatlakozás Engedélyezése",
|
||||
"enter_the_quick_connect_code": "Add meg a gyors csatlakozási kódot...",
|
||||
"success": "Siker",
|
||||
"quick_connect_autorized": "Gyorscsatlakozás Engedélyezve",
|
||||
"error": "Hiba",
|
||||
"invalid_code": "Érvénytelen Kód",
|
||||
"authorize": "Engedélyezés"
|
||||
},
|
||||
"media_controls": {
|
||||
"media_controls_title": "Médiavezérlés",
|
||||
"forward_skip_length": "Előre Ugrás Hossza",
|
||||
"rewind_length": "Visszatekerés Hossza",
|
||||
"seconds_unit": "mp"
|
||||
},
|
||||
"gesture_controls": {
|
||||
"gesture_controls_title": "Gesztusvezérlés",
|
||||
"horizontal_swipe_skip": "Vízszintes Húzás Ugráshoz",
|
||||
"horizontal_swipe_skip_description": "Ha a vezérlők el vannak rejtve, húzd balra vagy jobbra az ugráshoz.",
|
||||
"left_side_brightness": "Fényerő a Bal Oldalon",
|
||||
"left_side_brightness_description": "Húzd felfelé vagy lefelé a bal oldalon a fényerő állításához",
|
||||
"right_side_volume": "Fényerő a Jobb Oldalon",
|
||||
"right_side_volume_description": "Húzd felfelé vagy lefelé a jobb oldalon a hangerő állításához"
|
||||
},
|
||||
"audio": {
|
||||
"audio_title": "Hang",
|
||||
"set_audio_track": "Hangsáv Beállítása az Előző Elemből",
|
||||
"audio_language": "Hangsáv Nyelve",
|
||||
"audio_hint": "Válassz Alapértelmezett Hangsávnyelvet.",
|
||||
"none": "Nincs",
|
||||
"language": "Nyelv"
|
||||
},
|
||||
"subtitles": {
|
||||
"subtitle_title": "Feliratok",
|
||||
"subtitle_language": "Felirat Nyelve",
|
||||
"subtitle_mode": "Felirat Módja",
|
||||
"set_subtitle_track": "Feliratsáv Beállítása az Előző Elemből",
|
||||
"subtitle_size": "Felirat Mérete",
|
||||
"subtitle_hint": "Feliratbeállítások Megadása",
|
||||
"none": "Nincs",
|
||||
"language": "Nyelv",
|
||||
"loading": "Betöltés",
|
||||
"modes": {
|
||||
"Default": "Alapértelmezett",
|
||||
"Smart": "Intelligens",
|
||||
"Always": "Mindig",
|
||||
"None": "Nincs",
|
||||
"OnlyForced": "Csak Kényszerített"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"other_title": "Egyéb",
|
||||
"follow_device_orientation": "Automatikus Forgatás",
|
||||
"video_orientation": "Videó Tájolás",
|
||||
"orientation": "Tájolás",
|
||||
"orientations": {
|
||||
"DEFAULT": "Alapértelmezett",
|
||||
"ALL": "Összes",
|
||||
"PORTRAIT": "Álló",
|
||||
"PORTRAIT_UP": "Álló Felfelé",
|
||||
"PORTRAIT_DOWN": "Álló Lefelé",
|
||||
"LANDSCAPE": "Fekvő",
|
||||
"LANDSCAPE_LEFT": "Fekvő Balra",
|
||||
"LANDSCAPE_RIGHT": "Fekvő Jobbra",
|
||||
"OTHER": "Egyéb",
|
||||
"UNKNOWN": "Ismeretlen"
|
||||
},
|
||||
"safe_area_in_controls": "Biztonsági Sáv a Vezérlőkben",
|
||||
"video_player": "Videólejátszó",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
"VLC_4": "VLC 4 (Kísérleti + PiP)"
|
||||
},
|
||||
"show_custom_menu_links": "Egyéni Menülinkek Megjelenítése",
|
||||
"hide_libraries": "Könyvtárak Elrejtése",
|
||||
"select_liraries_you_want_to_hide": "Válaszd ki azokat a könyvtárakat, amelyeket el szeretnél rejteni a Könyvtár fülön és a kezdőlapon.",
|
||||
"disable_haptic_feedback": "Haptikus Visszajelzés Letiltása",
|
||||
"default_quality": "Alapértelmezett Minőség",
|
||||
"max_auto_play_episode_count": "Max. Auto. Epizódlejátszás",
|
||||
"disabled": "Letiltva"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Letöltések",
|
||||
"remux_max_download": "Remux Maximális Letöltés"
|
||||
},
|
||||
"plugins": {
|
||||
"plugins_title": "Bővítmények",
|
||||
"jellyseerr": {
|
||||
"jellyseerr_warning": "Ez az integráció még korai stádiumban van. Számíts a változásokra.",
|
||||
"server_url": "Szerver URL",
|
||||
"server_url_hint": "Példa: http(s)://a-te-szolgáltatód.url\n(adj meg portot, ha szükséges)",
|
||||
"server_url_placeholder": "Jellyseerr URL...",
|
||||
"password": "Jelszó",
|
||||
"password_placeholder": "Add meg a {{username}} Jellyfin felhasználó jelszavát",
|
||||
"login_button": "Bejelentkezés",
|
||||
"total_media_requests": "Összes Média Kérés",
|
||||
"movie_quota_limit": "Film Kvóta Limit",
|
||||
"movie_quota_days": "Film Kvóta Napok",
|
||||
"tv_quota_limit": "Sorozat Kvóta Limit",
|
||||
"tv_quota_days": "Sorozat Kvóta Napok",
|
||||
"reset_jellyseerr_config_button": "Jellyseerr Beállítások Visszaállítása",
|
||||
"unlimited": "Korlátlan",
|
||||
"plus_n_more": "+{{n}} További",
|
||||
"order_by": {
|
||||
"DEFAULT": "Alapértelmezett",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Szavazatok Száma és Átlag",
|
||||
"POPULARITY": "Népszerűség"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Keresés Engedélyezése",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://domain.org:port",
|
||||
"marlin_search_hint": "Add meg a Marlin szerver URL-jét. Az URL-nek tartalmaznia kell a http vagy https-t, és opcionálisan a portot.",
|
||||
"read_more_about_marlin": "Tudj Meg Többet a Marlinról",
|
||||
"save_button": "Mentés",
|
||||
"toasts": {
|
||||
"saved": "Mentve"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"storage_title": "Tárhely",
|
||||
"app_usage": "Alkalmazás {{usedSpace}}%",
|
||||
"device_usage": "Eszköz {{availableSpace}}%",
|
||||
"size_used": "{{used}} / {{total}} Használatban",
|
||||
"delete_all_downloaded_files": "Minden Letöltött Fájl Törlése"
|
||||
},
|
||||
"intro": {
|
||||
"show_intro": "Bemutató Megjelenítése",
|
||||
"reset_intro": "Bemutató Visszaállítása"
|
||||
},
|
||||
"logs": {
|
||||
"logs_title": "Naplók",
|
||||
"export_logs": "Naplók Exportálása",
|
||||
"click_for_more_info": "Kattints a Részletekért",
|
||||
"level": "Szint",
|
||||
"no_logs_available": "Nincsenek Naplók",
|
||||
"delete_all_logs": "Összes Napló Törlése"
|
||||
},
|
||||
"languages": {
|
||||
"title": "Nyelvek",
|
||||
"app_language": "Alkalmazás Nyelve",
|
||||
"system": "Rendszer"
|
||||
},
|
||||
"toasts": {
|
||||
"error_deleting_files": "Hiba a Fájlok Törlésekor"
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"title": "Munkamenetek",
|
||||
"no_active_sessions": "Nincsenek Aktív Munkamenetek"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Letöltések",
|
||||
"tvseries": "Sorozatok",
|
||||
"movies": "Filmek",
|
||||
"queue": "Sor",
|
||||
"queue_hint": "A sor és a letöltések az alkalmazás újraindításakor elvesznek",
|
||||
"no_items_in_queue": "Nincs Elem a Sorban",
|
||||
"no_downloaded_items": "Nincsenek Letöltött Elemek",
|
||||
"delete_all_movies_button": "Összes Film Törlése",
|
||||
"delete_all_tvseries_button": "Összes Sorozat Törlése",
|
||||
"delete_all_button": "Összes Törlése",
|
||||
"active_download": "Aktív Letöltés",
|
||||
"no_active_downloads": "Nincs Aktív Letöltés",
|
||||
"active_downloads": "Aktív Letöltések",
|
||||
"new_app_version_requires_re_download": "Az Új Alkalmazásverzió Újra Letöltést Igényel",
|
||||
"new_app_version_requires_re_download_description": "Az új frissítéshez az összes tartalmat újra le kell tölteni. Kérjük, töröld az összes letöltött tartalmat, majd próbáld újra.",
|
||||
"back": "Vissza",
|
||||
"delete": "Törlés",
|
||||
"something_went_wrong": "Hiba Történt",
|
||||
"could_not_get_stream_url_from_jellyfin": "Nem sikerült lekérni a stream URL-t a Jellyfinből",
|
||||
"eta": "Várható Idő: {{eta}}",
|
||||
"toasts": {
|
||||
"you_are_not_allowed_to_download_files": "Nem engedélyezett a fájlok letöltése.",
|
||||
"deleted_all_movies_successfully": "Az Összes Film Sikeresen Törölve!",
|
||||
"failed_to_delete_all_movies": "Nem Sikerült Törölni Az Összes Filmet",
|
||||
"deleted_all_tvseries_successfully": "Az Összes Sorozat Sikeresen Törölve!",
|
||||
"failed_to_delete_all_tvseries": "Nem Sikerült Törölni Az Összes Sorozatot",
|
||||
"download_deleted": "Letöltés Törölve",
|
||||
"could_not_delete_download": "Nem Sikerült Törölni a Letöltést",
|
||||
"download_paused": "Letöltés Szüneteltetve",
|
||||
"could_not_pause_download": "Nem Sikerült Szüneteltetni a Letöltést",
|
||||
"download_resumed": "Letöltés Folytatva",
|
||||
"could_not_resume_download": "Nem Sikerült Folytatni a Letöltést",
|
||||
"download_completed": "Letöltés Befejezve",
|
||||
"download_failed_for_item": "A(z) {{item}} letöltése sikertelen - {{error}}",
|
||||
"download_completed_for_item": "A(z) {{item}} letöltése befejezve",
|
||||
"all_files_folders_and_jobs_deleted_successfully": "Minden fájl, mappa és feladat sikeresen törölve",
|
||||
"go_to_downloads": "Ugrás a Letöltésekhez"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"search": "Keresés...",
|
||||
"x_items": "{{count}} Elem",
|
||||
"library": "Könyvtár",
|
||||
"discover": "Felfedezés",
|
||||
"no_results": "Nincs Eredmény",
|
||||
"no_results_found_for": "Nincs Eredmény a Kereséshez",
|
||||
"movies": "Filmek",
|
||||
"series": "Sorozatok",
|
||||
"episodes": "Epizódok",
|
||||
"collections": "Gyűjtemények",
|
||||
"actors": "Színészek",
|
||||
"request_movies": "Filmek Kérése",
|
||||
"request_series": "Sorozatok Kérése",
|
||||
"recently_added": "Legutóbb Hozzáadva",
|
||||
"recent_requests": "Legutóbbi Kérések",
|
||||
"plex_watchlist": "Plex Watchlist",
|
||||
"trending": "Népszerű",
|
||||
"popular_movies": "Népszerű Filmek",
|
||||
"movie_genres": "Film Műfajok",
|
||||
"upcoming_movies": "Hamarosan Megjelenő Filmek",
|
||||
"studios": "Stúdiók",
|
||||
"popular_tv": "Népszerű Sorozatok",
|
||||
"tv_genres": "Sorozat Műfajok",
|
||||
"upcoming_tv": "Hamarosan Megjelenő Sorozatok",
|
||||
"networks": "Csatornák",
|
||||
"tmdb_movie_keyword": "TMDB Film Kulcsszó",
|
||||
"tmdb_movie_genre": "TMDB Film Műfaj",
|
||||
"tmdb_tv_keyword": "TMDB Sorozat Kulcsszó",
|
||||
"tmdb_tv_genre": "TMDB Sorozat Műfaj",
|
||||
"tmdb_search": "TMDB Keresés",
|
||||
"tmdb_studio": "TMDB Stúdió",
|
||||
"tmdb_network": "TMDB Csatorna",
|
||||
"tmdb_movie_streaming_services": "TMDB Film Streaming Szolgáltatások",
|
||||
"tmdb_tv_streaming_services": "TMDB Sorozat Streaming Szolgáltatások"
|
||||
},
|
||||
"library": {
|
||||
"no_results": "Nincs Eredmény",
|
||||
"no_libraries_found": "Nem Található Könyvtár",
|
||||
"item_types": {
|
||||
"movies": "Filmek",
|
||||
"series": "Sorozatok",
|
||||
"boxsets": "Gyűjtemények",
|
||||
"items": "Elemek"
|
||||
},
|
||||
"options": {
|
||||
"display": "Megjelenítés",
|
||||
"row": "Sor",
|
||||
"list": "Lista",
|
||||
"image_style": "Kép Stílusa",
|
||||
"poster": "Poszter",
|
||||
"cover": "Borító",
|
||||
"show_titles": "Címek Megjelenítése",
|
||||
"show_stats": "Statisztikák Megjelenítése"
|
||||
},
|
||||
"filters": {
|
||||
"genres": "Műfajok",
|
||||
"years": "Évek",
|
||||
"sort_by": "Rendezés",
|
||||
"sort_order": "Rendezés Iránya",
|
||||
"tags": "Címkék"
|
||||
}
|
||||
},
|
||||
"favorites": {
|
||||
"series": "Sorozatok",
|
||||
"movies": "Filmek",
|
||||
"episodes": "Epizódok",
|
||||
"videos": "Videók",
|
||||
"boxsets": "Gyűjtemények",
|
||||
"playlists": "Lejátszási Listák",
|
||||
"noDataTitle": "Még Nincsenek Kedvencek",
|
||||
"noData": "Jelölj meg elemeket kedvencként, hogy itt gyorsan elérd őket."
|
||||
},
|
||||
"custom_links": {
|
||||
"no_links": "Nincsenek Linkek"
|
||||
},
|
||||
"player": {
|
||||
"error": "Hiba",
|
||||
"failed_to_get_stream_url": "Nem sikerült lekérni a stream URL-t",
|
||||
"an_error_occured_while_playing_the_video": "Hiba történt a videó lejátszása közben. Ellenőrizd a naplókat a beállításokban.",
|
||||
"client_error": "Kliens Hiba",
|
||||
"could_not_create_stream_for_chromecast": "A Chromecast stream létrehozása sikertelen volt",
|
||||
"message_from_server": "Üzenet a szervertől: {{message}}",
|
||||
"next_episode": "Következő Epizód",
|
||||
"refresh_tracks": "Sávok Frissítése",
|
||||
"audio_tracks": "Hangsávok:",
|
||||
"playback_state": "Lejátszás Állapota:",
|
||||
"index": "Index:",
|
||||
"continue_watching": "Folytatás",
|
||||
"go_back": "Vissza"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Következő",
|
||||
"no_items_to_display": "Nincs Megjeleníthető Elem",
|
||||
"cast_and_crew": "Szereplők & Stáb",
|
||||
"series": "Sorozat",
|
||||
"seasons": "Évadok",
|
||||
"season": "Évad",
|
||||
"no_episodes_for_this_season": "Ehhez az évadhoz nincs epizód",
|
||||
"overview": "Áttekintés",
|
||||
"more_with": "További {{name}} Alkotások",
|
||||
"similar_items": "Hasonló Elemek",
|
||||
"no_similar_items_found": "Nincs Hasonló Elem",
|
||||
"video": "Videó",
|
||||
"more_details": "További Részletek",
|
||||
"quality": "Minőség",
|
||||
"audio": "Hang",
|
||||
"subtitles": "Felirat",
|
||||
"show_more": "Több Megjelenítése",
|
||||
"show_less": "Kevesebb Megjelenítése",
|
||||
"appeared_in": "Megjelent:",
|
||||
"could_not_load_item": "Nem Sikerült Betölteni az Elemet",
|
||||
"none": "Nincs",
|
||||
"download": {
|
||||
"download_season": "Évad Letöltése",
|
||||
"download_series": "Sorozat Letöltése",
|
||||
"download_episode": "Epizód Letöltése",
|
||||
"download_movie": "Film Letöltése",
|
||||
"download_x_item": "{{item_count}} Elem Letöltése",
|
||||
"download_unwatched_only": "Csak Nem Megtekintett",
|
||||
"download_button": "Letöltés"
|
||||
}
|
||||
},
|
||||
"live_tv": {
|
||||
"next": "Következő",
|
||||
"previous": "Előző",
|
||||
"coming_soon": "Hamarosan",
|
||||
"on_now": "Most Műsoron",
|
||||
"shows": "Sorozatok",
|
||||
"movies": "Filmek",
|
||||
"sports": "Sport",
|
||||
"for_kids": "Gyerekeknek",
|
||||
"news": "Hírek"
|
||||
},
|
||||
"jellyseerr": {
|
||||
"confirm": "Megerősítés",
|
||||
"cancel": "Mégse",
|
||||
"yes": "Igen",
|
||||
"whats_wrong": "Mi a Probléma?",
|
||||
"issue_type": "Probléma Típusa",
|
||||
"select_an_issue": "Válassz Problémát",
|
||||
"types": "Típusok",
|
||||
"describe_the_issue": "(Opcionális) Fejtsd ki a problémát...",
|
||||
"submit_button": "Beküldés",
|
||||
"report_issue_button": "Probléma Jelentése",
|
||||
"request_button": "Kérés",
|
||||
"are_you_sure_you_want_to_request_all_seasons": "Biztosan az összes évadot kéred?",
|
||||
"failed_to_login": "Sikertelen Bejelentkezés",
|
||||
"cast": "Szereplők",
|
||||
"details": "Részletek",
|
||||
"status": "Állapot",
|
||||
"original_title": "Eredeti Cím",
|
||||
"series_type": "Sorozat Típusa",
|
||||
"release_dates": "Megjelenési Dátumok",
|
||||
"first_air_date": "Első Vetítés Dátuma",
|
||||
"next_air_date": "Következő Adás Dátuma",
|
||||
"revenue": "Bevétel",
|
||||
"budget": "Költségvetés",
|
||||
"original_language": "Eredeti Nyelv",
|
||||
"production_country": "Gyártási Ország",
|
||||
"studios": "Stúdiók",
|
||||
"network": "Csatorna",
|
||||
"currently_streaming_on": "Jelenleg Elérhető:",
|
||||
"advanced": "Haladó",
|
||||
"request_as": "Kérés Más Felhasználóként",
|
||||
"tags": "Címkék",
|
||||
"quality_profile": "Minőségi Profil",
|
||||
"root_folder": "Gyökérmappa",
|
||||
"season_all": "Évad (Összes)",
|
||||
"season_number": "Évad {{season_number}}",
|
||||
"number_episodes": "{{episode_number}} Epizód",
|
||||
"born": "Született",
|
||||
"appearances": "Megjelenések",
|
||||
"toasts": {
|
||||
"jellyseer_does_not_meet_requirements": "A Jellyseerr szerver nem felel meg a minimum verziókövetelményeknek! Kérlek frissítsd legalább 2.0.0-ra.",
|
||||
"jellyseerr_test_failed": "A Jellyseerr teszt sikertelen. Próbáld újra.",
|
||||
"failed_to_test_jellyseerr_server_url": "Nem sikerült tesztelni a Jellyseerr szerver URL-jét",
|
||||
"issue_submitted": "Probléma Beküldve!",
|
||||
"requested_item": "{{item}} Kérése Sikeres!",
|
||||
"you_dont_have_permission_to_request": "Nincs jogosultságod a kéréshez!",
|
||||
"something_went_wrong_requesting_media": "Hiba történt a média kérés közben!"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Kezdőlap",
|
||||
"search": "Keresés",
|
||||
"library": "Könyvtár",
|
||||
"custom_links": "Egyéni Linkek",
|
||||
"favorites": "Kedvencek"
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,13 @@ export type Settings = {
|
||||
defaultPlayer: VideoPlayer;
|
||||
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
|
||||
autoPlayEpisodeCount: number;
|
||||
vlcTextColor?: string;
|
||||
vlcBackgroundColor?: string;
|
||||
vlcOutlineColor?: string;
|
||||
vlcOutlineThickness?: string;
|
||||
vlcBackgroundOpacity?: number;
|
||||
vlcOutlineOpacity?: number;
|
||||
vlcIsBold?: boolean;
|
||||
// Gesture controls
|
||||
enableHorizontalSwipeSkip: boolean;
|
||||
enableLeftSideBrightnessSwipe: boolean;
|
||||
@@ -229,6 +236,13 @@ export const defaultValues: Settings = {
|
||||
defaultPlayer: VideoPlayer.VLC_3, // ios-only setting. does not matter what this is for android
|
||||
maxAutoPlayEpisodeCount: { key: "3", value: 3 },
|
||||
autoPlayEpisodeCount: 0,
|
||||
vlcTextColor: undefined,
|
||||
vlcBackgroundColor: undefined,
|
||||
vlcOutlineColor: undefined,
|
||||
vlcOutlineThickness: undefined,
|
||||
vlcBackgroundOpacity: undefined,
|
||||
vlcOutlineOpacity: undefined,
|
||||
vlcIsBold: undefined,
|
||||
// Gesture controls
|
||||
enableHorizontalSwipeSkip: true,
|
||||
enableLeftSideBrightnessSwipe: true,
|
||||
|
||||
Reference in New Issue
Block a user