Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .github/workflows/manual-docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Manual Docker Build & Push

on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to build'
required: true
default: 'develop'
type: string
tag:
description: 'Docker image tag (e.g., "my-feature", "test"). Leave empty for branch name.'
required: false
type: string
platforms:
description: 'Target platforms'
required: true
default: 'linux/amd64'
type: choice
options:
- linux/amd64
- linux/amd64,linux/arm64
- linux/amd64,linux/arm/v7,linux/arm64

jobs:
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-24.04
permissions:
packages: write
contents: read

steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
fetch-depth: 0

- name: Setup Node.js for WebUI
uses: actions/setup-node@v4
with:
node-version: 22

- name: Build WebUI
run: |
cd UI/Web || exit 1
echo 'Installing web dependencies'
npm ci

echo 'Building UI'
npm run prod

echo 'Copying back to Kavita wwwroot'
rsync -a dist/ ../../API/wwwroot/
cd ../..

- name: Get csproj Version
uses: kzrnm/get-net-sdk-project-versions-action@v2
id: get-version
with:
proj-path: Kavita.Common/Kavita.Common.csproj

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x

- name: Install Swashbuckle CLI
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli

- name: Build .NET Application
run: ./monorepo-build.sh

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Determine Docker Tag
id: docker-tag
run: |
if [ -n "${{ inputs.tag }}" ]; then
TAG="${{ inputs.tag }}"
else
# Use branch name, sanitized for Docker tags
TAG=$(echo "${{ inputs.branch }}" | sed 's/[^a-zA-Z0-9._-]/-/g')
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Using Docker tag: $TAG"

- name: Extract metadata for Docker
id: docker_meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/kavita
tags: |
type=raw,value=${{ steps.docker-tag.outputs.tag }}
type=raw,value=${{ steps.docker-tag.outputs.tag }}-${{ steps.get-version.outputs.assembly-version }}
type=sha,prefix=${{ steps.docker-tag.outputs.tag }}-

- name: Build and Push Docker Image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ inputs.platforms }}
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

- name: Summary
run: |
echo "## Docker Image Built Successfully! :rocket:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Branch:** \`${{ inputs.branch }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Version:** \`${{ steps.get-version.outputs.assembly-version }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Platforms:** \`${{ inputs.platforms }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Images pushed:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.docker_meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Digest:** \`${{ steps.docker_build.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from '../_models/narration-settings';
import { SettingItemComponent } from '../../settings/_components/setting-item/setting-item.component';
import { SettingSwitchComponent } from '../../settings/_components/setting-switch/setting-switch.component';
import { NgClass } from '@angular/common';
import { DecimalPipe, NgClass } from '@angular/common';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';

@Component({
Expand All @@ -36,7 +36,8 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
SettingItemComponent,
SettingSwitchComponent,
NgClass,
NgbTooltip
NgbTooltip,
DecimalPipe
]
})
export class ManageNarrationSettingsComponent implements OnInit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ <h5>{{ t('listen') }}</h5>
}

<!-- Generation in Progress -->
@if (!loading && chapterInfo?.isGenerating && chapterInfo.activeJob) {
@if (!loading && chapterInfo?.isGenerating && chapterInfo?.activeJob) {
<div class="generation-progress">
<div class="progress-header">
<i class="fa-solid fa-cog fa-spin"></i>
Expand All @@ -46,14 +46,14 @@ <h5>{{ t('listen') }}</h5>
<div class="progress-bar-container">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
[style.width.%]="chapterInfo.activeJob.progress">
[style.width.%]="chapterInfo?.activeJob?.progress || 0">
</div>
</div>
<span class="progress-text">{{ chapterInfo.activeJob.progress | number:'1.0-0' }}%</span>
<span class="progress-text">{{ (chapterInfo?.activeJob?.progress || 0) | number:'1.0-0' }}%</span>
</div>
@if (chapterInfo.activeJob.estimatedSecondsRemaining) {
@if (chapterInfo?.activeJob?.estimatedSecondsRemaining) {
<div class="eta">
{{ t('eta') }}: {{ formatTime(chapterInfo.activeJob.estimatedSecondsRemaining) }}
{{ t('eta') }}: {{ formatTime(chapterInfo?.activeJob?.estimatedSecondsRemaining || 0) }}
</div>
}
<button class="btn btn-sm btn-outline-danger mt-2" (click)="cancelGeneration()">
Expand All @@ -66,15 +66,15 @@ <h5>{{ t('listen') }}</h5>
@if (!loading && chapterInfo?.activeJob?.status === NarrationQueueStatus.Queued) {
<div class="generation-queued">
<i class="fa-solid fa-clock"></i>
<span>{{ t('queued') }} - {{ t('position') }}: {{ chapterInfo.activeJob.queuePosition }}</span>
<span>{{ t('queued') }} - {{ t('position') }}: {{ chapterInfo?.activeJob?.queuePosition }}</span>
<button class="btn btn-sm btn-outline-danger" (click)="cancelGeneration()">
{{ t('cancel') }}
</button>
</div>
}

<!-- Main Player (when narration is available) -->
@if (!loading && !error && chapterInfo?.hasNarration && !chapterInfo.isGenerating) {
@if (!loading && !error && chapterInfo?.hasNarration && !chapterInfo?.isGenerating) {
<div class="player-main" [class.minimized]="minimized">

<!-- Minimized View -->
Expand Down Expand Up @@ -145,6 +145,19 @@ <h5>{{ t('listen') }}</h5>

<!-- Secondary Controls -->
<div class="secondary-controls">
<!-- Volume Control -->
<div class="volume-control">
<i class="fa-solid" [ngClass]="{'fa-volume-high': volume > 0.5, 'fa-volume-low': volume > 0 && volume <= 0.5, 'fa-volume-xmark': volume === 0}"></i>
<input type="range"
class="volume-slider"
min="0"
max="1"
step="0.05"
[value]="volume"
(input)="onVolumeChange($event)"
[ngbTooltip]="t('volume') || 'Volume'">
</div>

<!-- Speed -->
<div ngbDropdown class="speed-dropdown">
<button class="btn btn-outline-secondary btn-sm" ngbDropdownToggle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@
justify-content: center;

&:hover {
background: var(--primary-color-dark, darken(var(--primary-color), 10%));
background: var(--primary-color-dark);
filter: brightness(0.9);
}
}

Expand Down Expand Up @@ -315,6 +316,7 @@
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
align-items: center;

.btn {
min-width: 80px;
Expand All @@ -333,6 +335,51 @@
.badge {
font-size: 0.625rem;
}

.volume-control {
display: flex;
align-items: center;
gap: 0.5rem;

i {
color: var(--text-muted-color);
font-size: 1rem;
width: 18px;
text-align: center;
}

.volume-slider {
width: 80px;
height: 4px;
appearance: none;
background: var(--input-bg-color);
border-radius: 2px;
cursor: pointer;

&::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
transition: transform 0.1s ease;

&:hover {
transform: scale(1.2);
}
}

&::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: none;
}
}
}
}

// Bookmarks Panel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class NarrationPlayerComponent implements OnInit, OnDestroy {
totalDuration = 0;
bufferedDuration = 0;
playbackSpeed = 1.0;
volume = 1.0; // 0.0 to 1.0

// Sleep timer
sleepTimerActive = false;
Expand Down Expand Up @@ -250,6 +251,19 @@ export class NarrationPlayerComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
}

setVolume(volume: number): void {
this.volume = Math.max(0, Math.min(1, volume));
if (this.audioElement?.nativeElement) {
this.audioElement.nativeElement.volume = this.volume;
}
this.cdRef.markForCheck();
}

onVolumeChange(event: Event): void {
const target = event.target as HTMLInputElement;
this.setVolume(parseFloat(target.value));
}

// Sleep timer
setSleepTimer(option: SleepTimerOption): void {
this.clearSleepTimer();
Expand Down Expand Up @@ -423,7 +437,7 @@ export class NarrationPlayerComponent implements OnInit, OnDestroy {
}
}

private calculateTotalTime(segmentIndex: number, positionInSegment: number): number {
protected calculateTotalTime(segmentIndex: number, positionInSegment: number): number {
if (!this.chapterInfo?.segments) return 0;

let totalTime = 0;
Expand Down
Loading