Merge branch 'main' into feature/discord-rpc

This commit is contained in:
Moustapha Kodjo Amadou 2025-07-21 09:33:45 +01:00 committed by GitHub
commit e290e0ad4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 2373 additions and 1334 deletions

View file

@ -258,7 +258,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install webkit2gtk-4.1 clang cmake ninja-build pkg-config libgtk-3-dev mpv libmpv-dev dpkg-dev libblkid-dev liblzma-dev fuse rpm imagemagick
sudo apt-get install webkit2gtk-4.1 clang cmake ninja-build pkg-config libgtk-3-dev mpv libmpv-dev dpkg-dev libblkid-dev liblzma-dev fuse rpm
wget -O /usr/local/bin/linuxdeploy "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
chmod +x /usr/local/bin/linuxdeploy
@ -291,11 +291,10 @@ jobs:
# Create fresh AppDir structure
rm -rf AppDir
mkdir -p AppDir/usr/bin
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
mkdir -p AppDir/usr/share/icons
# Copy built files
cp -r build/linux/x64/release/bundle/* AppDir/usr/bin/
convert -resize 256x256 assets/app_icons/icon-red.png AppDir/usr/share/icons/hicolor/256x256/apps/mangayomi.png
convert -resize 512x512 assets/app_icons/icon-red.png AppDir/mangayomi.png
cp -rL linux/packaging/icons/* AppDir/usr/share/icons
# Create desktop file in AppDir root
cp linux/mangayomi.desktop AppDir/mangayomi.desktop
# Create AppRun file
@ -312,7 +311,7 @@ jobs:
/usr/local/bin/linuxdeploy \
--appdir AppDir \
--desktop-file AppDir/mangayomi.desktop \
--icon-file AppDir/mangayomi.png \
--icon-file AppDir/usr/share/icons/hicolor/512x512/apps/mangayomi.png \
--executable AppDir/usr/bin/mangayomi \
--output appimage
mv $(find . -type f -name "*.AppImage") build/linux/x64/release/Mangayomi-${{ github.ref_name }}-linux.AppImage
@ -323,8 +322,8 @@ jobs:
mkdir -p rpm_build/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
cp -r build/linux/x64/release/bundle rpm_build/SOURCES/mangayomi-${VERSION}
tar czf rpm_build/SOURCES/mangayomi-${VERSION}.tar.gz -C rpm_build/SOURCES mangayomi-${VERSION}
mkdir -p rpm_build/SOURCES/usr/share/icons/hicolor/256x256/apps
cp assets/app_icons/icon-red.png rpm_build/SOURCES/usr/share/icons/hicolor/256x256/apps/mangayomi.png
mkdir -p rpm_build/SOURCES/usr/share/icons
cp -rL linux/packaging/icons/* rpm_build/SOURCES/usr/share/icons
cp linux/mangayomi.desktop rpm_build/SOURCES/
# RPM Spec File
cat <<EOF > rpm_build/SPECS/mangayomi.spec
@ -353,7 +352,12 @@ jobs:
%files
/usr/bin/*
/usr/share/applications/mangayomi.desktop
/usr/share/icons/hicolor/16x16/apps/mangayomi.png
/usr/share/icons/hicolor/32x32/apps/mangayomi.png
/usr/share/icons/hicolor/64x64/apps/mangayomi.png
/usr/share/icons/hicolor/128x128/apps/mangayomi.png
/usr/share/icons/hicolor/256x256/apps/mangayomi.png
/usr/share/icons/hicolor/512x512/apps/mangayomi.png
EOF
rpmbuild --define "_topdir $(pwd)/rpm_build" -ba rpm_build/SPECS/mangayomi.spec
@ -418,10 +422,11 @@ jobs:
dpkg-deb -R "$(find dist -name '*.deb' | head -n 1)" extracted_deb
# Replace desktop file
cp -f linux/mangayomi.desktop extracted_deb/usr/share/applications/mangayomi.desktop
# copy all icon sizes
cp -rLf linux/packaging/icons/* extracted_deb/usr/share/icons
# Set correct permissions
chmod 644 extracted_deb/usr/share/applications/mangayomi.desktop
# Repack the .deb
sudo apt-get install -y fakeroot
fakeroot dpkg-deb -b extracted_deb dist/Mangayomi-${{ github.ref_name }}-linux.deb
- name: upload artifact linux deb

362
go/build_go.sh Executable file
View file

@ -0,0 +1,362 @@
#!/bin/bash
# Go build script for Mangayomi
# It supports Linux, macOS, Windows, Android, and iOS builds.
# to build, run:
# ./build_go.sh [linux|macos|windows|android|ios] [--all]
set -e # Stop on error
# Colors for messages
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Colored message functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
# Check Go
if ! command -v go &> /dev/null; then
log_error "Go is not installed. Please install Go 1.22 or newer."
exit 1
fi
GO_VERSION=$(go version | grep -o 'go[0-9]\+\.[0-9]\+' | sed 's/go//')
log_info "Detected Go version: $GO_VERSION"
# Check if we're in the go directory
if [ ! -f "go.mod" ]; then
log_error "go.mod not found. Run this script from the 'go' directory of the Mangayomi project."
exit 1
fi
}
# Build for Linux
build_linux() {
log_info "Building for Linux..."
mkdir -p ../linux/bundle/lib
go build -buildmode=c-shared -ldflags="-s -w" -trimpath -o ../linux/bundle/lib/libmtorrentserver.so ./binding/desktop
log_success "Linux build completed: ../linux/bundle/lib/libmtorrentserver.so"
}
# Build for macOS
build_macos() {
log_info "Building for macOS..."
if [[ "$OSTYPE" != "darwin"* ]]; then
log_warning "macOS build requested but you're not on macOS. Build might fail."
fi
mkdir -p ../macos/Frameworks
# Build ARM64
log_info "Building macOS ARM64..."
CGO_ENABLED=1 GOARCH=arm64 go build -buildmode=c-shared -ldflags="-s -w" -trimpath -o libmtorrentserver_arm64.dylib ./binding/desktop
# Build AMD64
log_info "Building macOS AMD64..."
CGO_ENABLED=1 GOARCH=amd64 go build -buildmode=c-shared -ldflags="-s -w" -trimpath -o libmtorrentserver_amd64.dylib ./binding/desktop
# Create universal binary
if command -v lipo &> /dev/null; then
log_info "Creating universal binary..."
lipo -create -output ../macos/Frameworks/libmtorrentserver.dylib libmtorrentserver_arm64.dylib libmtorrentserver_amd64.dylib
# Clean up intermediate binaries
log_info "Cleaning up intermediate binaries..."
rm -f libmtorrentserver_arm64.dylib libmtorrentserver_amd64.dylib
rm -f libmtorrentserver_arm64.h libmtorrentserver_amd64.h
log_success "macOS build completed: ../macos/Frameworks/libmtorrentserver.dylib (universal)"
else
log_warning "lipo not available. Separate binaries are available."
fi
}
# Build for Windows
build_windows() {
log_info "Building for Windows..."
mkdir -p ../windows
# Configure environment for Windows
export CGO_ENABLED=1
export CC=gcc
export GOARCH=amd64
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
# Native Windows
go build -buildmode=c-shared -ldflags="-s -w -extldflags=-static" -trimpath -o ../windows/libmtorrentserver.dll ./binding/desktop
else
# Cross-compilation from Unix
if command -v x86_64-w64-mingw32-gcc &> /dev/null; then
export CC=x86_64-w64-mingw32-gcc
export GOOS=windows
go build -buildmode=c-shared -ldflags="-s -w -extldflags=-static" -trimpath -o ../windows/libmtorrentserver.dll ./binding/desktop
else
log_error "Windows cross-compiler not found. Install mingw-w64."
return 1
fi
fi
log_success "Windows build completed: ../windows/libmtorrentserver.dll"
}
# Build for Android
build_android() {
log_info "Building for Android..."
# Check gomobile
if ! command -v gomobile &> /dev/null; then
log_info "Installing gomobile..."
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gobind@latest
# Add GOPATH/bin to PATH if necessary
export PATH=$PATH:$(go env GOPATH)/bin
# Initialize gomobile
log_info "Initializing gomobile..."
gomobile init -v
fi
# Check Android NDK
if [ -z "$ANDROID_NDK_HOME" ] && [ -z "$ANDROID_NDK_ROOT" ]; then
log_warning "ANDROID_NDK_HOME or ANDROID_NDK_ROOT not defined. Android build might fail."
log_warning "Install Android Studio and NDK, then set the environment variable."
log_warning "Example: export ANDROID_NDK_HOME=/path/to/android/sdk/ndk/25.2.9519653"
fi
# Check Java JDK
if ! javac -version &> /dev/null; then
log_warning "javac (Java Development Kit) not working. Android build might fail."
log_warning "Install Oracle JDK or OpenJDK and configure JAVA_HOME."
log_warning "On macOS: brew install openjdk@11"
log_warning "Then: export JAVA_HOME=\$(brew --prefix openjdk@11)"
fi
go get -u golang.org/x/mobile
log_info "Building Android AAR..."
mkdir -p ../android/app/libs
# Try Android build with enhanced error handling
# Uses -checklinkname=0 and -tags to allow anet v0.0.5 usage (solution inspired by Gopeed)
if ! gomobile bind -v -tags mobile -target=android/arm,android/arm64,android/amd64,android/386 -androidapi 21 -ldflags="-s -w -checklinkname=0" -trimpath -o ../android/app/libs/libmtorrentserver.aar ./binding/mobile; then
log_error "Android build failed."
log_error "Common error: 'invalid reference to net.zoneCache'"
log_error ""
log_error "Possible solutions:"
log_error "1. Install and configure Android NDK correctly:"
log_error " - Download Android Studio"
log_error " - Install Android NDK via SDK Manager"
log_error " - Set ANDROID_NDK_HOME"
log_error ""
log_error "2. Update Go to latest version:"
log_error " - current go version: $(go version)"
log_error " - Recommended: Go 1.21 or newer"
log_error ""
log_error "3. Known issue with github.com/wlynxg/anet:"
log_error " - This package uses internal Go APIs"
log_error " - Applied solution: replace directive in go.mod"
log_error ""
log_error "4. Check Java JDK installation:"
log_error " - Install JDK: brew install openjdk@11"
log_error " - Set JAVA_HOME: export JAVA_HOME=\$(brew --prefix openjdk@11)"
log_error ""
log_error "5. Alternative - Desktop build only:"
log_error " ./build_go.sh linux macos windows"
return 1
fi
rm -f ../android/app/libs/libmtorrentserver-sources.jar
log_success "Android build completed: ../android/app/libs/libmtorrentserver.aar"
}
# Build for iOS
build_ios() {
log_info "Building for iOS..."
if [[ "$OSTYPE" != "darwin"* ]]; then
log_error "iOS build is only possible on macOS."
return 1
fi
# Check Xcode
if ! command -v xcodebuild &> /dev/null; then
log_error "Xcode is not installed."
return 1
fi
# Check gomobile
if ! command -v gomobile &> /dev/null; then
log_info "Installing gomobile..."
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gobind@latest
# Add GOPATH/bin to PATH if necessary
export PATH=$PATH:$(go env GOPATH)/bin
# Initialize gomobile
log_info "Initializing gomobile..."
gomobile init -v
fi
go get -u golang.org/x/mobile
log_info "Building iOS XCFramework..."
mkdir -p ../ios/Frameworks
# Uses same optimizations as Android for anet v0.0.5 compatibility
gomobile bind -v -tags mobile -target=ios,iossimulator -ldflags="-s -w -checklinkname=0" -trimpath -o ../ios/Frameworks/libmtorrentserver.xcframework ./binding/mobile
log_success "iOS build completed: ../ios/Frameworks/libmtorrentserver.xcframework"
}
# Show help
show_help() {
echo "Usage: $0 [OPTIONS] [TARGETS]"
echo ""
echo "OPTIONS:"
echo " -h, --help Show this help"
echo " -a, --all Build for all supported platforms"
echo ""
echo "TARGETS:"
echo " linux Build for Linux"
echo " macos Build for macOS"
echo " windows Build for Windows"
echo " android Build for Android"
echo " ios Build for iOS"
echo ""
echo "Examples:"
echo " $0 linux macos # Build for Linux and macOS"
}
# Variables
BUILD_ALL=false
TARGETS=()
# Parser les arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-a|--all)
BUILD_ALL=true
shift
;;
linux|macos|windows|android|ios)
TARGETS+=("$1")
shift
;;
*)
log_error "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Main
main() {
log_info "Starting Go build for Mangayomi"
check_prerequisites
if [ "$BUILD_ALL" = true ]; then
TARGETS=("linux" "macos" "windows" "android" "ios")
fi
if [ ${#TARGETS[@]} -eq 0 ]; then
log_warning "No target specified. Building for current platform..."
case "$OSTYPE" in
linux*) TARGETS=("linux") ;;
darwin*) TARGETS=("macos") ;;
msys*|win32*) TARGETS=("windows") ;;
*)
log_error "Unsupported platform: $OSTYPE"
show_help
exit 1
;;
esac
fi
# Build for each target
for target in "${TARGETS[@]}"; do
log_info "--- Building for: $target ---"
case $target in
linux)
if build_linux; then
log_success "✅ Build $target successful"
else
log_error "❌ Build $target failed"
fi
;;
macos)
if build_macos; then
log_success "✅ Build $target successful"
else
log_error "❌ Build $target failed"
fi
;;
windows)
if build_windows; then
log_success "✅ Build $target successful"
else
log_error "❌ Build $target failed"
fi
;;
android)
if build_android; then
log_success "✅ Build $target successful"
else
log_error "❌ Build $target failed"
log_warning "Android build may fail due to compatibility issues"
log_warning "Other platforms should work normally"
fi
;;
ios)
if build_ios; then
log_success "✅ Build $target successful"
else
log_error "❌ Build $target failed"
fi
;;
*)
log_error "Unknown target: $target"
;;
esac
echo "" # Empty line for readability
done
}
# Execute main script
main "$@"

112
go/go.mod
View file

@ -1,9 +1,11 @@
module server
go 1.22
go 1.23.0
toolchain go1.24.3
require (
github.com/anacrolix/torrent v1.56.1
github.com/anacrolix/torrent v1.58.1
github.com/rs/cors v1.11.1
)
@ -11,38 +13,38 @@ require (
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
github.com/anacrolix/chansync v0.5.1 // indirect
github.com/anacrolix/dht/v2 v2.21.1 // indirect
github.com/anacrolix/envpprof v1.3.0 // indirect
github.com/anacrolix/generics v0.0.2 // indirect
github.com/anacrolix/go-libutp v1.3.1 // indirect
github.com/anacrolix/log v0.15.2 // indirect
github.com/anacrolix/chansync v0.6.0 // indirect
github.com/anacrolix/dht/v2 v2.22.1 // indirect
github.com/anacrolix/envpprof v1.4.0 // indirect
github.com/anacrolix/generics v0.0.3 // indirect
github.com/anacrolix/go-libutp v1.3.2 // indirect
github.com/anacrolix/log v0.16.0 // indirect
github.com/anacrolix/missinggo v1.3.0 // indirect
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
github.com/anacrolix/missinggo/v2 v2.7.3 // indirect
github.com/anacrolix/mmsg v1.0.0 // indirect
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
github.com/anacrolix/missinggo/v2 v2.8.0 // indirect
github.com/anacrolix/mmsg v1.1.1 // indirect
github.com/anacrolix/multiless v0.4.0 // indirect
github.com/anacrolix/stm v0.5.0 // indirect
github.com/anacrolix/sync v0.5.1 // indirect
github.com/anacrolix/sync v0.5.4 // indirect
github.com/anacrolix/upnp v0.1.4 // indirect
github.com/anacrolix/utp v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/benbjohnson/immutable v0.4.3 // indirect
github.com/bits-and-blooms/bitset v1.14.2 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/go-llsqlite/adapter v0.1.0 // indirect
github.com/go-llsqlite/crawshaw v0.5.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/go-llsqlite/adapter v0.2.0 // indirect
github.com/go-llsqlite/crawshaw v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
@ -50,45 +52,47 @@ require (
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/ice/v2 v2.3.34 // indirect
github.com/pion/interceptor v0.1.30 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.40 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.14 // indirect
github.com/pion/rtp v1.8.9 // indirect
github.com/pion/sctp v1.8.33 // indirect
github.com/pion/sdp/v3 v3.0.9 // indirect
github.com/pion/srtp/v2 v2.0.20 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pion/webrtc/v3 v3.3.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.20 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.14 // indirect
github.com/pion/srtp/v3 v3.0.6 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pion/webrtc/v4 v4.1.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/wlynxg/anet v0.0.4 // indirect
go.etcd.io/bbolt v1.3.11 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/time v0.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
modernc.org/libc v1.60.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.32.0 // indirect
zombiezen.com/go/sqlite v1.3.0 // indirect
github.com/tidwall/btree v1.8.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.etcd.io/bbolt v1.4.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/time v0.12.0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.0 // indirect
zombiezen.com/go/sqlite v1.4.2 // indirect
)
replace github.com/wlynxg/anet => github.com/wlynxg/anet v0.0.5

301
go/go.sum
View file

@ -26,26 +26,26 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anacrolix/chansync v0.5.1 h1:j+R9DtotkXm40VFjZ8rJTSJkg2Gv1ldZt8kl96lyJJ0=
github.com/anacrolix/chansync v0.5.1/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
github.com/anacrolix/dht/v2 v2.21.1 h1:s1rKkfLLcmBHKv4v/mtMkIeHIEptzEFiB6xVu54+5/o=
github.com/anacrolix/dht/v2 v2.21.1/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
github.com/anacrolix/chansync v0.6.0 h1:/aQVvZ1yLRhmqEYrr9dC92JwzNBQ/SNnFi4uk+fTkQY=
github.com/anacrolix/chansync v0.6.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
github.com/anacrolix/dht/v2 v2.22.1 h1:mgsljPXyA/EWA7uUDSNjx7wf6gsfhppjVIp9auVeR6w=
github.com/anacrolix/dht/v2 v2.22.1/go.mod h1:seXRz6HLw8zEnxlysf9ye2eQbrKUmch6PyOHpe/Nb/U=
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
github.com/anacrolix/envpprof v1.4.0 h1:QHeIcrgHcRChhnxR8l6rlaLlRQx9zd7Q2NII6Zbt83w=
github.com/anacrolix/envpprof v1.4.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/generics v0.0.2 h1:UbtD+KntUGxeGYMC4RwhsETieL9ixGdSptJQRhdy7No=
github.com/anacrolix/generics v0.0.2/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
github.com/anacrolix/generics v0.0.3 h1:wMkQgQzq0obSy1tMkxDu7Ife7PsegOBWHDRaSW31EnM=
github.com/anacrolix/generics v0.0.3/go.mod h1:MN3ve08Z3zSV/rTuX/ouI4lNdlfTxgdafQJiLzyNRB8=
github.com/anacrolix/go-libutp v1.3.2 h1:WswiaxTIogchbkzNgGHuHRfbrYLpv4o290mlvcx+++M=
github.com/anacrolix/go-libutp v1.3.2/go.mod h1:fCUiEnXJSe3jsPG554A200Qv+45ZzIIyGEvE56SHmyA=
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
github.com/anacrolix/log v0.15.2 h1:LTSf5Wm6Q4GNWPFMBP7NPYV6UBVZzZLKckL+/Lj72Oo=
github.com/anacrolix/log v0.15.2/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA=
github.com/anacrolix/log v0.16.0 h1:DSuyb5kAJwl3Y0X1TRcStVrTS9ST9b0BHW+7neE4Xho=
github.com/anacrolix/log v0.16.0/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
@ -58,25 +58,25 @@ github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
github.com/anacrolix/missinggo/v2 v2.7.3 h1:Ee//CmZBMadeNiYB/hHo9ly2PFOEZ4Fhsbnug3rDAIE=
github.com/anacrolix/missinggo/v2 v2.7.3/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 h1:lOtCD+LzoD1g7bowhYJNR++uV+FyY5bTZXKwnPex9S8=
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
github.com/anacrolix/missinggo/v2 v2.8.0 h1:6pGnVOlR6TWL9JM5Msyezij8YHU3+oHO7r82Eql/kpA=
github.com/anacrolix/missinggo/v2 v2.8.0/go.mod h1:vVO5FEziQm+NFmJesc7StpkquZk+WJFCaL0Wp//2sa0=
github.com/anacrolix/mmsg v1.0.1/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
github.com/anacrolix/mmsg v1.1.1 h1:4ce/3I5kM7qSF6T5A8MOmDCfac3UqYlk5Bzh5XsWebM=
github.com/anacrolix/mmsg v1.1.1/go.mod h1:lPCXEN1eDDQtKktdKEzdw+roswx6wWPpeXAl/WpWVDU=
github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM=
github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc=
github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
github.com/anacrolix/sync v0.5.4 h1:yXZLIjXh/G+Rh2mYGCAPmszmF/fvEPadDy7/pPChpKM=
github.com/anacrolix/sync v0.5.4/go.mod h1:21cUWerw9eiu/3T3kyoChu37AVO+YFue1/H15qqubS0=
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
github.com/anacrolix/torrent v1.56.1 h1:QeJMOP0NuhpQ5dATsOqEL0vUO85aPMNMGP2FACNt0Eg=
github.com/anacrolix/torrent v1.56.1/go.mod h1:5DMHbeIM1TuC5wTQ99XieKKLiYZYz6iB2lyZpKZEr6w=
github.com/anacrolix/torrent v1.58.1 h1:6FP+KH57b1gyT2CpVL9fEqf9MGJEgh3xw1VA8rI0pW8=
github.com/anacrolix/torrent v1.58.1/go.mod h1:/7ZdLuHNKgtCE1gjYJCfbtG9JodBcDaF5ip5EUWRtk8=
github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
@ -91,8 +91,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.14.2 h1:YXVoyPndbdvcEVcseEovVfp0qjJp7S+i5+xgp/Nfbdc=
github.com/bits-and-blooms/bitset v1.14.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
@ -113,8 +113,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -128,17 +128,19 @@ github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1T
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-llsqlite/adapter v0.1.0 h1:wGSQNsu/rtYeu/lqZNZQMjwUdEF3OW66xTLvsFwJQUw=
github.com/go-llsqlite/adapter v0.1.0/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
github.com/go-llsqlite/crawshaw v0.5.5 h1:sXnRkiV26MBv++lbPbzp+ZzFcTqzVMxftO8yHyFvwUA=
github.com/go-llsqlite/crawshaw v0.5.5/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
github.com/go-llsqlite/adapter v0.2.0 h1:6k4dmTSTg1eKIeH+2kBWaoohn9SFNZeg4LWayZweevI=
github.com/go-llsqlite/adapter v0.2.0/go.mod h1:tcIEbwjdknnizwMsq9ogjMW6246aIjk97cRywjkbqZ0=
github.com/go-llsqlite/crawshaw v0.6.0 h1:3c0p/CU4EFG2zhSkXLwM2Bgt8ZNqwUgA6wimxkxqC1c=
github.com/go-llsqlite/crawshaw v0.6.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -165,10 +167,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -179,8 +180,6 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gortc/stun v1.20.0 h1:7FNyWwjRXVByBItBwpnsv6SZ08UzPfmByLjKmOnpb14=
github.com/gortc/stun v1.20.0/go.mod h1:/XeODKxk0b1P5pYtdD/ZlOncR7+6XgU4zb9CzJIFJBs=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@ -197,8 +196,8 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -237,51 +236,38 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/ice/v2 v2.3.34 h1:Ic1ppYCj4tUOcPAp76U6F3fVrlSw8A9JtRXLqw6BbUM=
github.com/pion/ice/v2 v2.3.34/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
github.com/pion/interceptor v0.1.30 h1:au5rlVHsgmxNi+v/mjOPazbW1SHzfx7/hYOEYQnUcxA=
github.com/pion/interceptor v0.1.30/go.mod h1:RQuKT5HTdkP2Fi0cuOS5G5WNymTjzXaGF75J4k7z2nc=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/stun v1.17.3 h1:JioTPckDp7PdfoF1FYSz1/sOWrXimbjyfZzvg2QIinU=
github.com/pion/stun v1.17.3/go.mod h1:4iy9kiYvpncdXoYYJoAvZ4YFybb4/gQmZxUNaU2680Y=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.3.0 h1:Rf4u6n6U5t5sUxhYPQk/samzU/oDv7jk6BA5hyO2F9I=
github.com/pion/webrtc/v3 v3.3.0/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0=
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -305,13 +291,16 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
@ -331,7 +320,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -340,56 +328,55 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/btree v1.8.0 h1:kHHy8hSBauQUe0KPHMFLOt0olAj1nDnkHPJhr8+HFkM=
github.com/tidwall/btree v1.8.0/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.4 h1:0de1OFQxnNqAu+x2FAKKCVIrnfGKQbs7FQz++tB0+Uw=
github.com/wlynxg/anet v0.0.4/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 h1:Z6EFcPz8e1cx0ge5jWCwqafndPjdsDQf8fk4Kw3pJoI=
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0/go.mod h1:kqVs191xxTTCd39tk8zK1UD3jyCS1SPrMHTpJ9ujxZg=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -405,13 +392,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -423,8 +407,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -445,20 +429,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -466,12 +445,11 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -482,8 +460,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -505,7 +483,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@ -519,31 +496,33 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s=
modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY=
zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=
zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=

View file

@ -1,6 +1,5 @@
package server
//credits: https://github.com/glblduh/StreamRest
import (
"context"
"encoding/json"
@ -11,7 +10,9 @@ import (
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
@ -20,41 +21,156 @@ import (
"github.com/rs/cors"
)
var torrentCli *torrent.Client
var torrentcliCfg *torrent.ClientConfig
var (
torrentCli *torrent.Client
torrentcliCfg *torrent.ClientConfig
// Cache for torrent metadata
torrentCache = sync.Map{}
// Worker pool for I/O operations
workerPool *WorkerPool
)
func Start(config *Config) (int, error) {
// WorkerPool manages a pool of goroutines for concurrent tasks
type WorkerPool struct {
workers int
jobQueue chan func()
quit chan bool
wg sync.WaitGroup
}
torrentcliCfg = torrent.NewDefaultClientConfig()
torrentcliCfg.DataDir = filepath.Clean(config.Path)
log.Printf("[INFO] Download directory is set to: %s\n", torrentcliCfg.DataDir)
var torrentCliErr error
torrentCli, torrentCliErr = torrent.NewClient(torrentcliCfg)
if torrentCliErr != nil {
log.Fatalf("[ERROR] Creation of BitTorrent client failed: %s\n", torrentCliErr)
// NewWorkerPool creates a new worker pool
func NewWorkerPool(workers int) *WorkerPool {
if workers <= 0 {
workers = runtime.NumCPU()
}
dnsResolve()
pool := &WorkerPool{
workers: workers,
jobQueue: make(chan func(), workers*2),
quit: make(chan bool),
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
log.Println("[INFO] Termination detected. Removing torrents")
for _, t := range torrentCli.Torrents() {
log.Printf("[INFO] Removing torrent: [%s]\n", t.Name())
t.Drop()
rmaErr := os.RemoveAll(filepath.Join(torrentcliCfg.DataDir, t.Name()))
if rmaErr != nil {
log.Printf("[ERROR] Failed to remove files of torrent: [%s]: %s\n", t.Name(), rmaErr)
}
pool.start()
return pool
}
// start starts the workers
func (p *WorkerPool) start() {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go p.worker()
}
}
// worker executes the jobs
func (p *WorkerPool) worker() {
defer p.wg.Done()
for {
select {
case job := <-p.jobQueue:
job()
case <-p.quit:
return
}
}
}
// Submit submits a job to the pool
func (p *WorkerPool) Submit(job func()) {
select {
case p.jobQueue <- job:
default:
// If pool is full, execute directly
go job()
}
}
// Stop stops the pool
func (p *WorkerPool) Stop() {
close(p.quit)
p.wg.Wait()
}
// TorrentMetadata caches torrent metadata
type TorrentMetadata struct {
InfoHash string
Name string
Files []FileMetadata
UpdatedAt time.Time
}
// FileMetadata contains file information
type FileMetadata struct {
Path string
Size int64
StreamURL string
}
func Start(config *Config) (int, error) {
// Initialize worker pool
workerPool = NewWorkerPool(runtime.NumCPU() * 2)
torrentcliCfg = torrent.NewDefaultClientConfig()
torrentcliCfg.DataDir = filepath.Clean(config.Path)
// Performance optimizations
torrentcliCfg.DisableUTP = false
torrentcliCfg.NoDHT = false
torrentcliCfg.NoDefaultPortForwarding = false
torrentcliCfg.DisablePEX = false
torrentcliCfg.AcceptPeerConnections = true
torrentcliCfg.EstablishedConnsPerTorrent = 80
torrentcliCfg.HalfOpenConnsPerTorrent = 25
torrentcliCfg.TorrentPeersHighWater = 200
torrentcliCfg.TorrentPeersLowWater = 50
log.Printf("[INFO] Download directory: %s", torrentcliCfg.DataDir)
log.Printf("[INFO] Worker pool size: %d", workerPool.workers)
var err error
torrentCli, err = torrent.NewClient(torrentcliCfg)
if err != nil {
log.Fatalf("[ERROR] BitTorrent client creation failed: %s", err)
}
// Optimized DNS configuration
configureDNS()
// HTTP server configuration with optimized timeouts
mux := setupRoutes()
c := configureCORS()
listener, err := net.Listen("tcp", config.Address)
if err != nil {
return 0, err
}
addr := listener.Addr().(*net.TCPAddr)
// Optimized HTTP server
server := &http.Server{
Handler: c.Handler(mux),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
ReadHeaderTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
// Graceful shutdown handling
setupGracefulShutdown(server)
log.Printf("[INFO] Server listening on %s", addr.AddrPort())
go func() {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Printf("[ERROR] Server error: %s", err)
}
os.Exit(0)
}()
return addr.Port, nil
}
func setupRoutes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/torrent/addmagnet", addMagnet)
mux.HandleFunc("/torrent/stream", streamTorrent)
@ -63,45 +179,85 @@ func Start(config *Config) (int, error) {
mux.HandleFunc("/torrent/play", playTorrent)
mux.HandleFunc("/torrent/add", AddTorrent)
mux.HandleFunc("/", Init)
return mux
}
c := cors.New(cors.Options{
func configureCORS() *cors.Cors {
return cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "DELETE"},
AllowCredentials: true,
MaxAge: 86400, // 24h cache
})
}
listener, err := net.Listen("tcp", config.Address)
if err != nil {
return 0, err
}
addr := listener.Addr().(*net.TCPAddr)
log.Printf("[INFO] Listening on %s\n", addr.AddrPort())
func setupGracefulShutdown(server *http.Server) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := http.Serve(listener, c.Handler(mux)); err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
<-sigs
log.Println("[INFO] Shutdown signal received")
return addr.Port, nil
// Graceful server shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("[ERROR] Server shutdown error: %s", err)
}
// Torrent cleanup
cleanupTorrents()
// Stop worker pool
if workerPool != nil {
workerPool.Stop()
}
os.Exit(0)
}()
}
func cleanupTorrents() {
log.Println("[INFO] Cleaning up torrents...")
for _, t := range torrentCli.Torrents() {
log.Printf("[INFO] Removing torrent: [%s]", t.Name())
t.Drop()
if err := os.RemoveAll(filepath.Join(torrentcliCfg.DataDir, t.Name())); err != nil {
log.Printf("[ERROR] Failed to remove torrent files [%s]: %s", t.Name(), err)
}
}
if torrentCli != nil {
torrentCli.Close()
}
}
func safenDisplayPath(displayPath string) string {
fileNameArray := strings.Split(displayPath, "/")
return strings.Join(fileNameArray, " ")
return strings.ReplaceAll(displayPath, "/", " ")
}
func appendFilePlaylist(scheme string, host string, infohash string, name string) string {
playList := "#EXTINF:-1," + safenDisplayPath(name) + "\n"
playList += scheme + "://" + host + "/torrent/stream?infohash=" + infohash + "&file=" + url.QueryEscape(name) + "\n"
return playList
func appendFilePlaylist(scheme, host, infohash, name string) string {
var sb strings.Builder
sb.WriteString("#EXTINF:-1,")
sb.WriteString(safenDisplayPath(name))
sb.WriteString("\n")
sb.WriteString(scheme)
sb.WriteString("://")
sb.WriteString(host)
sb.WriteString("/torrent/stream?infohash=")
sb.WriteString(infohash)
sb.WriteString("&file=")
sb.WriteString(url.QueryEscape(name))
sb.WriteString("\n")
return sb.String()
}
func nameCheck(str string, substr string) bool {
splittedSubStr := strings.Split(substr, " ")
for _, curWord := range splittedSubStr {
if !strings.Contains(str, curWord) {
func nameCheck(str, substr string) bool {
strLower := strings.ToLower(str)
words := strings.Fields(strings.ToLower(substr))
for _, word := range words {
if !strings.Contains(strLower, word) {
return false
}
}
@ -109,140 +265,269 @@ func nameCheck(str string, substr string) bool {
}
func getTorrentFile(files []*torrent.File, filename string, exactName bool) *torrent.File {
var tFile *torrent.File = nil
if filename == "" && !exactName {
return nil
}
for _, file := range files {
if exactName && file.DisplayPath() == filename {
tFile = file
return file
}
if !exactName && filename != "" && nameCheck(strings.ToLower(file.DisplayPath()), strings.ToLower(filename)) {
tFile = file
}
if tFile != nil {
break
if !exactName && nameCheck(file.DisplayPath(), filename) {
return file
}
}
return tFile
return nil
}
// https://github.com/YouROK/TorrServer/blob/681fc5c343f6d2782dee0c015d2ba2dfd210f88f/server/cmd/main.go#L114
func dnsResolve() {
addrs, err := net.LookupHost("www.google.com")
if len(addrs) == 0 {
log.Printf("Check dns failed", addrs, err)
// configureDNS optimizes DNS resolution
func configureDNS() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
fn := func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", "1.1.1.1:53")
}
addrs, err := net.DefaultResolver.LookupHost(ctx, "www.google.com")
if len(addrs) == 0 || err != nil {
log.Printf("[WARN] DNS check failed, using Cloudflare DNS: %v", err)
net.DefaultResolver = &net.Resolver{
Dial: fn,
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 5 * time.Second}
return d.DialContext(ctx, "udp", "1.1.1.1:53")
},
}
addrs, err = net.LookupHost("www.google.com")
log.Printf("Check cloudflare dns", addrs, err)
// Test with Cloudflare DNS
ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()
addrs, err = net.DefaultResolver.LookupHost(ctx2, "www.google.com")
log.Printf("[INFO] Cloudflare DNS test: %v, error: %v", len(addrs) > 0, err)
} else {
log.Printf("Check dns OK", addrs, err)
log.Printf("[INFO] DNS check OK: %d addresses", len(addrs))
}
}
func makePlayStreamURL(infohash string, filename string, isStream bool) string {
endPoint := "play"
// getCachedTorrentMetadata retrieves metadata from cache
func getCachedTorrentMetadata(infoHash string) (*TorrentMetadata, bool) {
if cached, ok := torrentCache.Load(infoHash); ok {
meta := cached.(*TorrentMetadata)
// Cache valid for 5 minutes
if time.Since(meta.UpdatedAt) < 5*time.Minute {
return meta, true
}
// Remove expired cache
torrentCache.Delete(infoHash)
}
return nil, false
}
// cacheTorrentMetadata caches metadata
func cacheTorrentMetadata(t *torrent.Torrent) *TorrentMetadata {
meta := &TorrentMetadata{
InfoHash: t.InfoHash().String(),
Name: t.Name(),
UpdatedAt: time.Now(),
}
if t.Info() != nil {
meta.Files = make([]FileMetadata, 0, len(t.Files()))
for _, file := range t.Files() {
meta.Files = append(meta.Files, FileMetadata{
Path: file.DisplayPath(),
Size: file.Length(),
StreamURL: makePlayStreamURL(meta.InfoHash, file.DisplayPath(), true),
})
}
}
torrentCache.Store(meta.InfoHash, meta)
return meta
}
func makePlayStreamURL(infohash, filename string, isStream bool) string {
var sb strings.Builder
sb.WriteString("/torrent/")
if isStream {
endPoint = "stream"
sb.WriteString("stream")
} else {
sb.WriteString("play")
}
URL := "/torrent/" + endPoint + "?infohash=" + infohash
sb.WriteString("?infohash=")
sb.WriteString(infohash)
if filename != "" {
URL += "&file=" + url.QueryEscape(filename)
sb.WriteString("&file=")
sb.WriteString(url.QueryEscape(filename))
}
return URL
return sb.String()
}
func httpJSONError(w http.ResponseWriter, error string, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if json.NewEncoder(w).Encode(errorRes{
Error: error,
}) != nil {
if err := json.NewEncoder(w).Encode(errorRes{Error: error}); err != nil {
http.Error(w, error, code)
}
}
func parseRequestBody(w http.ResponseWriter, r *http.Request, v any) error {
err := json.NewDecoder(r.Body).Decode(v)
func parseRequestBody(w http.ResponseWriter, r *http.Request, v interface{}) error {
defer r.Body.Close()
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Additional security
err := decoder.Decode(v)
if err != nil {
httpJSONError(w, "Request JSON body decode error", http.StatusInternalServerError)
httpJSONError(w, "Request JSON body decode error", http.StatusBadRequest)
}
return err
}
func makeJSONResponse(w http.ResponseWriter, v any) {
func makeJSONResponse(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(v)
if err != nil {
encoder := json.NewEncoder(w)
if err := encoder.Encode(v); err != nil {
httpJSONError(w, "Response JSON body encode error", http.StatusInternalServerError)
}
}
func getInfo(t *torrent.Torrent) {
if t != nil {
<-t.GotInfo()
// getInfoWithTimeout waits for torrent information with timeout
func getInfoWithTimeout(t *torrent.Torrent, timeout time.Duration) error {
if t == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
select {
case <-t.GotInfo():
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func initMagnet(w http.ResponseWriter, magnet string, alldn []string, alltr []string) *torrent.Torrent {
var t *torrent.Torrent = nil
var err error
magnetString := magnet
func initMagnet(w http.ResponseWriter, magnet string, alldn, alltr []string) *torrent.Torrent {
var sb strings.Builder
sb.WriteString(magnet)
for _, dn := range alldn {
magnetString += "&dn=" + url.QueryEscape(dn)
sb.WriteString("&dn=")
sb.WriteString(url.QueryEscape(dn))
}
for _, tr := range alltr {
magnetString += "&tr=" + url.QueryEscape(tr)
sb.WriteString("&tr=")
sb.WriteString(url.QueryEscape(tr))
}
t, err = torrentCli.AddMagnet(magnetString)
t, err := torrentCli.AddMagnet(sb.String())
if err != nil {
httpJSONError(w, "Torrent add error", http.StatusInternalServerError)
httpJSONError(w, "Torrent add error: "+err.Error(), http.StatusInternalServerError)
return nil
}
getInfo(t)
// Wait for information with timeout
if err := getInfoWithTimeout(t, 30*time.Second); err != nil {
log.Printf("[WARN] Timeout waiting for torrent info: %s", err)
}
// Cache metadata
cacheTorrentMetadata(t)
return t
}
func getTorrent(w http.ResponseWriter, infoHash string) *torrent.Torrent {
var t *torrent.Torrent = nil
var tOk bool
if len(infoHash) != 40 {
httpJSONError(w, "InfoHash not valid", http.StatusInternalServerError)
return t
httpJSONError(w, "InfoHash not valid", http.StatusBadRequest)
return nil
}
t, tOk = torrentCli.Torrent(metainfo.NewHashFromHex(infoHash))
if !tOk {
// Check cache first
if _, found := getCachedTorrentMetadata(infoHash); found {
if t, ok := torrentCli.Torrent(metainfo.NewHashFromHex(infoHash)); ok {
return t
}
// Remove from cache if torrent no longer exists
torrentCache.Delete(infoHash)
}
t, ok := torrentCli.Torrent(metainfo.NewHashFromHex(infoHash))
if !ok {
httpJSONError(w, "Torrent not found", http.StatusNotFound)
return nil
}
getInfo(t)
if t == nil {
httpJSONError(w, "Torrent is nil", http.StatusInternalServerError)
return nil
}
// Wait for information with timeout
if err := getInfoWithTimeout(t, 10*time.Second); err != nil {
log.Printf("[WARN] Timeout waiting for torrent info: %s", err)
// Don't return nil here, torrent may still be usable
}
// Cache metadata only if we have the info
if t.Info() != nil {
cacheTorrentMetadata(t)
}
return t
}
func parseTorrentStats(t *torrent.Torrent) torrentStatsRes {
// Protection against nil pointers
if t == nil {
return torrentStatsRes{}
}
// Use cache if available
if meta, found := getCachedTorrentMetadata(t.InfoHash().String()); found {
stats := t.Stats()
return torrentStatsRes{
InfoHash: meta.InfoHash,
Name: meta.Name,
TotalPeers: stats.TotalPeers,
ActivePeers: stats.ActivePeers,
HalfOpenPeers: stats.HalfOpenPeers,
PendingPeers: stats.PendingPeers,
Files: buildFilesFromCache(meta, t),
}
}
// Fallback to original method
var tsRes torrentStatsRes
tsRes.InfoHash = t.InfoHash().String()
tsRes.Name = t.Name()
tsRes.TotalPeers = t.Stats().TotalPeers
tsRes.ActivePeers = t.Stats().ActivePeers
tsRes.HalfOpenPeers = t.Stats().HalfOpenPeers
tsRes.PendingPeers = t.Stats().PendingPeers
stats := t.Stats()
tsRes.TotalPeers = stats.TotalPeers
tsRes.ActivePeers = stats.ActivePeers
tsRes.HalfOpenPeers = stats.HalfOpenPeers
tsRes.PendingPeers = stats.PendingPeers
if t.Info() == nil {
return tsRes
}
for _, tFile := range t.Files() {
files := t.Files()
if files == nil {
return tsRes
}
tsRes.Files.OnTorrent = make([]torrentStatsFilesOnTorrent, 0, len(files))
for _, tFile := range files {
if tFile == nil {
continue
}
tsRes.Files.OnTorrent = append(tsRes.Files.OnTorrent, torrentStatsFilesOnTorrent{
FileName: tFile.DisplayPath(),
FileSizeBytes: int(tFile.Length()),
})
if tFile.BytesCompleted() != 0 {
if tFile.BytesCompleted() > 0 {
tsRes.Files.OnDisk = append(tsRes.Files.OnDisk, torrentStatsFilesOnDisk{
FileName: tFile.DisplayPath(),
StreamURL: makePlayStreamURL(t.InfoHash().String(), tFile.DisplayPath(), true),
@ -252,9 +537,38 @@ func parseTorrentStats(t *torrent.Torrent) torrentStatsRes {
}
}
// Update cache only if we have complete info
if t.Info() != nil {
cacheTorrentMetadata(t)
}
return tsRes
}
func buildFilesFromCache(meta *TorrentMetadata, t *torrent.Torrent) torrentStatsFiles {
var files torrentStatsFiles
files.OnTorrent = make([]torrentStatsFilesOnTorrent, 0, len(meta.Files))
for _, fileMeta := range meta.Files {
files.OnTorrent = append(files.OnTorrent, torrentStatsFilesOnTorrent{
FileName: fileMeta.Path,
FileSizeBytes: int(fileMeta.Size),
})
// Check if file is downloaded (requires access to real torrent)
if tFile := getTorrentFile(t.Files(), fileMeta.Path, true); tFile != nil && tFile.BytesCompleted() > 0 {
files.OnDisk = append(files.OnDisk, torrentStatsFilesOnDisk{
FileName: fileMeta.Path,
StreamURL: fileMeta.StreamURL,
BytesDownloaded: int(tFile.BytesCompleted()),
FileSizeBytes: int(fileMeta.Size),
})
}
}
return files
}
func addMagnet(w http.ResponseWriter, r *http.Request) {
var amBody addMagnetBody
var amRes addMagnetRes
@ -317,108 +631,273 @@ func addMagnet(w http.ResponseWriter, r *http.Request) {
}
func streamTorrent(w http.ResponseWriter, r *http.Request) {
infoHash, ihOk := r.URL.Query()["infohash"]
fileName, fnOk := r.URL.Query()["file"]
query := r.URL.Query()
infoHashParams, ihOk := query["infohash"]
fileNameParams, fnOk := query["file"]
if !ihOk || !fnOk {
httpJSONError(w, "InfoHash or File is not provided", http.StatusNotFound)
if !ihOk || !fnOk || len(infoHashParams) == 0 || len(fileNameParams) == 0 {
httpJSONError(w, "InfoHash or File is not provided", http.StatusBadRequest)
return
}
t := getTorrent(w, infoHash[0])
infoHash := infoHashParams[0]
fileName := fileNameParams[0]
t := getTorrent(w, infoHash)
if t == nil {
return
}
tFile := getTorrentFile(t.Files(), fileName[0], true)
tFile := getTorrentFile(t.Files(), fileName, true)
if tFile == nil {
httpJSONError(w, "File not found", http.StatusNotFound)
return
}
fileRead := tFile.NewReader()
defer fileRead.Close()
fileRead.SetReadahead(tFile.Length() / 100)
http.ServeContent(w, r, tFile.DisplayPath(), time.Now(), fileRead)
fileReader := tFile.NewReader()
defer fileReader.Close()
readaheadSize := calculateReadahead(tFile.Length())
fileReader.SetReadahead(readaheadSize)
// Headers for optimized streaming
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Type", getContentType(fileName))
w.Header().Set("Cache-Control", "public, max-age=3600")
http.ServeContent(w, r, tFile.DisplayPath(), time.Now(), fileReader)
}
// calculateReadahead calculates optimal readahead size
func calculateReadahead(fileSize int64) int64 {
switch {
case fileSize < 100*1024*1024: // < 100MB
return fileSize / 20 // 5%
case fileSize < 1024*1024*1024: // < 1GB
return fileSize / 50 // 2%
default:
return fileSize / 100 // 1%
}
}
// getContentType determines file MIME type
func getContentType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
// Video formats
case ".mp4":
return "video/mp4"
case ".mkv":
return "video/x-matroska"
case ".avi":
return "video/x-msvideo"
case ".mov":
return "video/quicktime"
case ".wmv":
return "video/x-ms-wmv"
case ".webm":
return "video/webm"
case ".flv":
return "video/x-flv"
case ".m4v":
return "video/x-m4v"
case ".3gp":
return "video/3gpp"
case ".3g2":
return "video/3gpp2"
case ".ts":
return "video/mp2t"
case ".mts":
return "video/mp2t"
case ".m2ts":
return "video/mp2t"
case ".vob":
return "video/dvd"
case ".ogv":
return "video/ogg"
case ".asf":
return "video/x-ms-asf"
case ".rm":
return "application/vnd.rn-realmedia"
case ".rmvb":
return "application/vnd.rn-realmedia-vbr"
case ".f4v":
return "video/x-f4v"
case ".mpg", ".mpeg":
return "video/mpeg"
case ".m1v":
return "video/mpeg"
case ".m2v":
return "video/mpeg"
case ".divx":
return "video/divx"
case ".xvid":
return "video/x-msvideo"
// Audio formats
case ".mp3":
return "audio/mpeg"
case ".flac":
return "audio/flac"
case ".wav":
return "audio/wav"
case ".aac":
return "audio/aac"
case ".ogg":
return "audio/ogg"
case ".wma":
return "audio/x-ms-wma"
case ".m4a":
return "audio/mp4"
case ".opus":
return "audio/opus"
case ".ac3":
return "audio/ac3"
case ".dts":
return "audio/dts"
default:
return "application/octet-stream"
}
}
func removeTorrent(w http.ResponseWriter, r *http.Request) {
infoHash, ihOk := r.URL.Query()["infohash"]
if !ihOk {
httpJSONError(w, "InfoHash is not provided", http.StatusNotFound)
query := r.URL.Query()
infoHashParams, ihOk := query["infohash"]
if !ihOk || len(infoHashParams) == 0 {
httpJSONError(w, "InfoHash is not provided", http.StatusBadRequest)
return
}
t := getTorrent(w, infoHash[0])
infoHash := infoHashParams[0]
t := getTorrent(w, infoHash)
if t == nil {
httpJSONError(w, "Torrent not found", http.StatusNotFound)
return
}
t.Drop()
name := t.Name()
if os.RemoveAll(filepath.Join(torrentcliCfg.DataDir, t.Name())) != nil {
// Immediate response before cleanup
w.WriteHeader(http.StatusOK)
w.Write([]byte("Torrent removal initiated"))
httpJSONError(w, "ERROR WHEN REMOVING FILE", http.StatusInternalServerError)
return
// Asynchronous deletion after response
workerPool.Submit(func() {
t.Drop()
}
// Clean cache
torrentCache.Delete(infoHash)
// Remove files
if err := os.RemoveAll(filepath.Join(torrentcliCfg.DataDir, name)); err != nil {
log.Printf("[ERROR] Failed to remove torrent files [%s]: %s", name, err)
return
}
log.Printf("[INFO] Successfully removed torrent: %s", name)
})
}
func listTorrents(w http.ResponseWriter, r *http.Request) {
infoHash, ihOk := r.URL.Query()["infohash"]
var ltRes listTorrentsRes
query := r.URL.Query()
infoHashParams, ihOk := query["infohash"]
var ltRes listTorrentsRes
allTorrents := torrentCli.Torrents()
if ihOk {
allTorrents = nil
t := getTorrent(w, infoHash[0])
if ihOk && len(infoHashParams) > 0 {
t := getTorrent(w, infoHashParams[0])
if t == nil {
return
}
allTorrents = append(allTorrents, t)
allTorrents = []*torrent.Torrent{t}
}
for _, t := range allTorrents {
ltRes.Torrents = append(ltRes.Torrents, parseTorrentStats(t))
// Parallel processing of statistics
type result struct {
stats torrentStatsRes
index int
}
if !ihOk && len(ltRes.Torrents) < 1 {
w.WriteHeader(404)
results := make(chan result, len(allTorrents))
var wg sync.WaitGroup
for i, t := range allTorrents {
wg.Add(1)
go func(idx int, torrent *torrent.Torrent) {
defer wg.Done()
stats := parseTorrentStats(torrent)
results <- result{stats: stats, index: idx}
}(i, t)
}
go func() {
wg.Wait()
close(results)
}()
// Collect results in order
statsMap := make(map[int]torrentStatsRes)
for res := range results {
statsMap[res.index] = res.stats
}
// Rebuild list in order
ltRes.Torrents = make([]torrentStatsRes, 0, len(allTorrents))
for i := 0; i < len(allTorrents); i++ {
if stats, exists := statsMap[i]; exists {
ltRes.Torrents = append(ltRes.Torrents, stats)
}
}
if !ihOk && len(ltRes.Torrents) == 0 {
w.WriteHeader(http.StatusNotFound)
return
}
makeJSONResponse(w, &ltRes)
}
func AddTorrent(w http.ResponseWriter, request *http.Request) {
func AddTorrent(w http.ResponseWriter, r *http.Request) {
// Limit upload file size
r.ParseMultipartForm(100 << 20) // 100MB max
a, _, error := request.FormFile("file")
if error != nil {
log.Printf("error upload torrent")
w.WriteHeader(http.StatusForbidden)
file, _, err := r.FormFile("file")
if err != nil {
log.Printf("[ERROR] Upload error: %s", err)
httpJSONError(w, "File upload error", http.StatusBadRequest)
return
}
defer file.Close()
metainf, err := metainfo.Load(file)
if err != nil {
log.Printf("[ERROR] MetaInfo load error: %s", err)
httpJSONError(w, "Invalid torrent file", http.StatusBadRequest)
return
}
metainf, er_ := metainfo.Load(a)
if er_ != nil {
log.Printf("error error when loading MetaInfo")
w.WriteHeader(http.StatusForbidden)
return
}
torrent, err := torrentCli.AddTorrent(metainf)
if err != nil {
log.Print(err.Error())
w.WriteHeader(http.StatusForbidden)
log.Printf("[ERROR] Add torrent error: %s", err)
httpJSONError(w, "Failed to add torrent", http.StatusInternalServerError)
return
}
// Wait for information with timeout
if err := getInfoWithTimeout(torrent, 30*time.Second); err != nil {
log.Printf("[WARN] Timeout waiting for torrent info: %s", err)
}
// Cache metadata
cacheTorrentMetadata(torrent)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(torrent.InfoHash().HexString()))
}
func Init(w http.ResponseWriter, request *http.Request) {
w.Write([]byte("Torrent server is running"))
func Init(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("Torrent server is running - Optimized version"))
}
func playTorrent(w http.ResponseWriter, r *http.Request) {

View file

@ -169,7 +169,7 @@ enum _AniSkipPhase { none, opening, ending }
bool _firstTime = true;
class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
with TickerProviderStateMixin {
with TickerProviderStateMixin, WidgetsBindingObserver {
late final GlobalKey<VideoState> _key = GlobalKey<VideoState>();
late final useLibass = ref.read(useLibassStateProvider);
late final Player _player = Player(
@ -340,23 +340,11 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
_completed;
_currentTotalDurationSub;
_loadAndroidFont().then((_) {
_player.open(
Media(
_video.value!.videoTrack!.id,
httpHeaders: _video.value!.headers,
start: _streamController.geTCurrentPosition(),
),
);
_openMedia(_video.value!, _streamController.geTCurrentPosition());
if (widget.isTorrent) {
Future.delayed(const Duration(seconds: 10)).then((_) {
if (mounted) {
_player.open(
Media(
_video.value!.videoTrack!.id,
httpHeaders: _video.value!.headers,
start: _streamController.geTCurrentPosition(),
),
);
_openMedia(_video.value!, _streamController.geTCurrentPosition());
}
});
}
@ -365,6 +353,25 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
});
discordRpc.showChapterDetails(ref, widget.episode);
_currentPosition.addListener(_updateRpcTimestamp);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
_setCurrentPosition(true);
}
}
Future<void> _openMedia(VideoPrefs prefs, [Duration? position]) {
return _player.open(
Media(
prefs.videoTrack!.id,
httpHeaders: prefs.headers,
start: position ?? _currentPosition.value,
),
);
}
Future<void> _loadAndroidFont() async {
@ -413,17 +420,27 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
@override
void dispose() {
_currentPosition.removeListener(_updateRpcTimestamp);
WidgetsBinding.instance.removeObserver(this);
_setCurrentPosition(true);
_player.dispose();
_currentPositionSub.cancel();
_currentTotalDurationSub.cancel();
_completed.cancel();
_video.dispose();
_playbackSpeed.dispose();
_isDoubleSpeed.dispose();
_currentTotalDuration.dispose();
_showFitLabel.dispose();
_isCompleted.dispose();
_tempPosition.dispose();
_fit.dispose();
if (!_isDesktop) {
_setLandscapeMode(false);
}
_skipPhase.dispose();
discordRpc.showIdleText();
discordRpc.showOriginalTimestamp();
_currentPosition.dispose();
super.dispose();
}
@ -508,27 +525,20 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
selected,
),
onTap: () async {
if (_video.value?.videoTrack?.id == quality.videoTrack?.id) {
Navigator.pop(context);
return;
}
_video.value = quality;
_player.stop();
if (quality.isLocal) {
if (widget.isLocal) {
_player.setVideoTrack(quality.videoTrack!);
} else {
_player.open(
Media(
quality.videoTrack!.id,
httpHeaders: quality.headers,
start: _currentPosition.value,
),
);
_openMedia(quality);
}
} else {
_player.open(
Media(
quality.videoTrack!.id,
httpHeaders: quality.headers,
start: _currentPosition.value,
),
);
_openMedia(quality);
}
_initSubtitleAndAudio = true;
Navigator.pop(context);

View file

@ -49,7 +49,7 @@ class _DesktopControllerWidgetState
bool visible = true;
bool cursorVisible = true;
Duration controlsTransitionDuration = const Duration(milliseconds: 300);
Color backdropColor = const Color(0x66000000);
// Color backdropColor = const Color(0x66000000);
Timer? _timer;
int swipeDuration = 0; // Duration to seek in video
@ -63,6 +63,7 @@ class _DesktopControllerWidgetState
final List<StreamSubscription> subscriptions = [];
DateTime last = DateTime.now();
Timer? _tapTimer;
@override
void setState(VoidCallback fn) {
@ -98,6 +99,9 @@ class _DesktopControllerWidgetState
for (final subscription in subscriptions) {
subscription.cancel();
}
subscriptions.clear();
_timer?.cancel();
_tapTimer?.cancel();
super.dispose();
}
@ -146,8 +150,8 @@ class _DesktopControllerWidgetState
_timer?.cancel();
}
final bool modifyVolumeOnScroll = true;
final bool toggleFullscreenOnDoublePress = true;
final bool modifyVolumeOnScroll = true; // TODO. The variable is never changed
final bool toggleFullscreenOnDoublePress = true; // TODO. variable not changed
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
@ -250,6 +254,14 @@ class _DesktopControllerWidgetState
}
: null,
child: GestureDetector(
onTap: () {
// use own timer with onTapUp instead of onDoubleTap.
// onDoubleTap uses 300ms which feels laggy when pausing
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/constants.dart#L35
_tapTimer = Timer(const Duration(milliseconds: 100), () {
widget.videoController.player.playOrPause();
});
},
onLongPressStart: (e) {
previousPlaybackSpeed =
widget.videoController.player.state.rate;
@ -274,6 +286,8 @@ class _DesktopControllerWidgetState
final difference = now.difference(last);
last = now;
if (difference < const Duration(milliseconds: 400)) {
_tapTimer?.cancel();
_tapTimer = null;
final fullScreen = widget.desktopFullScreenPlayer;
await _changeFullScreen(ref, fullScreen);
}

View file

@ -51,6 +51,7 @@ class _MobileControllerWidgetState
);
final ValueNotifier<double> _brightnessValue = ValueNotifier(0.0);
final ValueNotifier<bool> _brightnessIndicator = ValueNotifier(false);
StreamSubscription<double>? _brightnessSubscription;
Timer? _brightnessTimer;
final ValueNotifier<double> _volumeValue = ValueNotifier(0.0);
@ -127,6 +128,15 @@ class _MobileControllerWidgetState
for (final subscription in subscriptions) {
subscription.cancel();
}
_timer?.cancel();
_volumeTimer?.cancel();
_brightnessTimer?.cancel();
_volumeValue.dispose();
_volumeIndicator.dispose();
_brightnessValue.dispose();
_brightnessIndicator.dispose();
_brightnessSubscription?.cancel();
_volumeController.removeListener();
// package:screen_brightness
Future.microtask(() async {
@ -134,7 +144,6 @@ class _MobileControllerWidgetState
await ScreenBrightness.instance.resetApplicationScreenBrightness();
} catch (_) {}
});
super.dispose();
}
@ -240,13 +249,14 @@ class _MobileControllerWidgetState
Future.microtask(() async {
try {
_brightnessValue.value = await ScreenBrightness.instance.application;
ScreenBrightness.instance.onApplicationScreenBrightnessChanged.listen((
value,
) {
if (mounted) {
_brightnessValue.value = value;
}
});
_brightnessSubscription = ScreenBrightness
.instance
.onApplicationScreenBrightnessChanged
.listen((value) {
if (mounted) {
_brightnessValue.value = value;
}
});
} catch (_) {}
});
}

View file

@ -22,7 +22,8 @@ class BrowseScreen extends ConsumerStatefulWidget {
class _BrowseScreenState extends ConsumerState<BrowseScreen>
with TickerProviderStateMixin {
late final hideItems = ref.watch(hideItemsStateProvider);
late final hideItems = ref.read(hideItemsStateProvider);
final _textEditingController = TextEditingController();
late TabController _tabBarController;
late final _tabList = [
@ -35,10 +36,9 @@ class _BrowseScreenState extends ConsumerState<BrowseScreen>
];
@override
void didChangeDependencies() {
super.didChangeDependencies();
void initState() {
super.initState();
_tabBarController = TabController(length: _tabList.length, vsync: this);
_tabBarController.animateTo(0);
_tabBarController.addListener(() {
_chekPermission();
setState(() {
@ -52,7 +52,13 @@ class _BrowseScreenState extends ConsumerState<BrowseScreen>
await StorageProvider().requestPermission();
}
final _textEditingController = TextEditingController();
@override
void dispose() {
_tabBarController.dispose();
_textEditingController.dispose();
super.dispose();
}
bool _isSearch = false;
@override
Widget build(BuildContext context) {

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:json_view/json_view.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -59,13 +61,14 @@ class _CodeEditorPageState extends ConsumerState<CodeEditorPage> {
[],
);
late final _logStreamController = Logger.logStreamController;
late final StreamSubscription _logSubscription;
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
_controller.text = source?.sourceCode ?? "";
useLogger = true;
_logStreamController.stream.asBroadcastStream().listen((event) async {
_logSubscription = _logStreamController.stream.listen((event) async {
_logsNotifier.value.add(event);
try {
await Future.delayed(const Duration(milliseconds: 5));
@ -133,11 +136,12 @@ class _CodeEditorPageState extends ConsumerState<CodeEditorPage> {
@override
void dispose() {
super.dispose();
_logSubscription.cancel();
_logsNotifier.value.clear();
_scrollController.dispose();
_controller.dispose();
useLogger = false;
super.dispose();
}
@override

View file

@ -37,6 +37,12 @@ class _ExtensionScreenState extends ConsumerState<ExtensionScreen> {
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
Future<void> _updateSource(Source source) {
return ref.read(
fetchItemSourcesListProvider(

View file

@ -298,6 +298,13 @@ class EditTextDialogWidget extends StatefulWidget {
class _EditTextDialogWidgetState extends State<EditTextDialogWidget> {
late final _controller = TextEditingController(text: widget.text);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(

View file

@ -35,7 +35,7 @@ class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
String query = "";
final _textEditingController = TextEditingController();
late final List<Source> sourceList =
ref.watch(onlyIncludePinnedSourceStateProvider)
ref.read(onlyIncludePinnedSourceStateProvider)
? isar.sources
.filter()
.isPinnedEqualTo(true)
@ -97,6 +97,12 @@ class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
: Container(),
);
}
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
}
class SourceSearchScreen extends StatefulWidget {

View file

@ -24,6 +24,13 @@ class SourcesScreen extends ConsumerStatefulWidget {
class _SourcesScreenState extends ConsumerState<SourcesScreen> {
final controller = ScrollController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context)!;

View file

@ -33,8 +33,8 @@ class HistoryScreen extends ConsumerStatefulWidget {
class _HistoryScreenState extends ConsumerState<HistoryScreen>
with TickerProviderStateMixin {
final _textEditingController = TextEditingController();
late TabController _tabBarController;
int tabs = 3;
void tabListener() {
setState(() {
@ -46,171 +46,150 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen>
@override
void initState() {
super.initState();
_tabBarController = TabController(length: tabs, vsync: this);
_tabBarController.animateTo(0);
final hideItems = ref.read(hideItemsStateProvider);
final tabCount = [
if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary",
if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary",
if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary",
].length;
_tabBarController = TabController(length: tabCount, vsync: this);
_tabBarController.addListener(tabListener);
}
final _textEditingController = TextEditingController();
@override
void dispose() {
_tabBarController.dispose();
_textEditingController.dispose();
super.dispose();
}
bool _isSearch = false;
List<History> entriesData = [];
// List<History> _entriesData = []; // TODO. The variable is never used/modified
@override
Widget build(BuildContext context) {
int newTabs = 0;
final hideItems = ref.watch(hideItemsStateProvider);
if (!hideItems.contains("/MangaLibrary")) newTabs++;
if (!hideItems.contains("/AnimeLibrary")) newTabs++;
if (!hideItems.contains("/NovelLibrary")) newTabs++;
if (newTabs == 0) {
return SizedBox.shrink();
}
if (tabs != newTabs) {
_tabBarController.removeListener(tabListener);
_tabBarController.dispose();
_tabBarController = TabController(length: newTabs, vsync: this);
_tabBarController.animateTo(0);
_tabBarController.addListener(tabListener);
setState(() {
tabs = newTabs;
});
}
final l10n = l10nLocalizations(context)!;
return DefaultTabController(
animationDuration: Duration.zero,
length: newTabs,
child: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
title: _isSearch
? null
: Text(
l10n.history,
style: TextStyle(color: Theme.of(context).hintColor),
),
actions: [
_isSearch
? SeachFormTextField(
onChanged: (value) {
setState(() {});
},
onSuffixPressed: () {
_textEditingController.clear();
setState(() {});
},
onPressed: () {
setState(() {
_isSearch = false;
});
_textEditingController.clear();
},
controller: _textEditingController,
)
: IconButton(
splashRadius: 20,
onPressed: () {
setState(() {
_isSearch = true;
});
},
icon: Icon(
Icons.search,
color: Theme.of(context).hintColor,
),
),
IconButton(
splashRadius: 20,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.remove_everything),
content: Text(l10n.remove_everything_msg),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () => clearHistory(hideItems),
child: Text(l10n.ok),
),
],
),
],
);
},
);
},
icon: Icon(
Icons.delete_sweep_outlined,
color: Theme.of(context).hintColor,
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
title: _isSearch
? null
: Text(
l10n.history,
style: TextStyle(color: Theme.of(context).hintColor),
),
actions: [
_isSearch
? SeachFormTextField(
onChanged: (value) {
setState(() {});
},
onSuffixPressed: () {
_textEditingController.clear();
setState(() {});
},
onPressed: () {
setState(() {
_isSearch = false;
});
_textEditingController.clear();
},
controller: _textEditingController,
)
: IconButton(
splashRadius: 20,
onPressed: () {
setState(() {
_isSearch = true;
});
},
icon: Icon(Icons.search, color: Theme.of(context).hintColor),
),
IconButton(
splashRadius: 20,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.remove_everything),
content: Text(l10n.remove_everything_msg),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () async {
if (mounted) Navigator.pop(context);
await _clearHistory(hideItems);
},
child: Text(l10n.ok),
),
],
),
],
);
},
);
},
icon: Icon(
Icons.delete_sweep_outlined,
color: Theme.of(context).hintColor,
),
),
],
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
controller: _tabBarController,
tabs: [
if (!hideItems.contains("/MangaLibrary")) Tab(text: l10n.manga),
if (!hideItems.contains("/AnimeLibrary")) Tab(text: l10n.anime),
if (!hideItems.contains("/NovelLibrary")) Tab(text: l10n.novel),
],
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
controller: _tabBarController,
tabs: [
if (!hideItems.contains("/MangaLibrary")) Tab(text: l10n.manga),
if (!hideItems.contains("/AnimeLibrary")) Tab(text: l10n.anime),
if (!hideItems.contains("/NovelLibrary")) Tab(text: l10n.novel),
],
),
),
body: Padding(
padding: const EdgeInsets.only(top: 10),
child: TabBarView(
controller: _tabBarController,
children: [
if (!hideItems.contains("/MangaLibrary"))
HistoryTab(
itemType: ItemType.manga,
query: _textEditingController.text,
),
if (!hideItems.contains("/AnimeLibrary"))
HistoryTab(
itemType: ItemType.anime,
query: _textEditingController.text,
),
if (!hideItems.contains("/NovelLibrary"))
HistoryTab(
itemType: ItemType.novel,
query: _textEditingController.text,
),
],
),
),
),
body: TabBarView(
controller: _tabBarController,
children: [
if (!hideItems.contains("/MangaLibrary"))
HistoryTab(
itemType: ItemType.manga,
query: _textEditingController.text,
),
if (!hideItems.contains("/AnimeLibrary"))
HistoryTab(
itemType: ItemType.anime,
query: _textEditingController.text,
),
if (!hideItems.contains("/NovelLibrary"))
HistoryTab(
itemType: ItemType.novel,
query: _textEditingController.text,
),
],
),
);
}
void clearHistory(List<String> hideItems) {
List<History> histories = isar.historys
Future<void> _clearHistory(List<String> hideItems) async {
List<History> histories = await isar.historys
.filter()
.idIsNotNull()
.chapter(
(q) =>
q.manga((q) => q.itemTypeEqualTo(getCurrentItemType(hideItems))),
)
.findAllSync()
.toList();
isar.writeTxnSync(() {
for (var history in histories) {
isar.historys.deleteSync(history.id!);
}
});
if (mounted) {
Navigator.pop(context);
}
.findAll();
final List<Id> idsToDelete = histories.map((h) => h.id!).toList();
await isar.writeTxn(() => isar.historys.deleteAll(idsToDelete));
}
ItemType getCurrentItemType(List<String> hideItems) {
@ -233,9 +212,13 @@ class HistoryTab extends ConsumerStatefulWidget {
ConsumerState<HistoryTab> createState() => _HistoryTabState();
}
class _HistoryTabState extends ConsumerState<HistoryTab> {
class _HistoryTabState extends ConsumerState<HistoryTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
final l10n = l10nLocalizations(context)!;
final history = ref.watch(
getAllHistoryStreamProvider(
@ -243,182 +226,179 @@ class _HistoryTabState extends ConsumerState<HistoryTab> {
search: widget.query,
),
);
return Scaffold(
body: history.when(
data: (entries) {
if (entries.isNotEmpty) {
return CustomScrollView(
slivers: [
CustomSliverGroupedListView<History, String>(
elements: entries,
groupBy: (element) => dateFormat(
element.date!,
context: context,
ref: ref,
forHistoryValue: true,
useRelativeTimesTamps: false,
),
groupSeparatorBuilder: (String groupByValue) => Padding(
padding: const EdgeInsets.only(bottom: 8, left: 12),
child: Row(
children: [
Text(
dateFormat(
null,
context: context,
stringDate: groupByValue,
ref: ref,
),
return history.when(
data: (entries) {
if (entries.isNotEmpty) {
return CustomScrollView(
slivers: [
CustomSliverGroupedListView<History, String>(
elements: entries,
groupBy: (element) => dateFormat(
element.date!,
context: context,
ref: ref,
forHistoryValue: true,
useRelativeTimesTamps: false,
),
groupSeparatorBuilder: (String groupByValue) => Padding(
padding: const EdgeInsets.only(bottom: 8, left: 12),
child: Row(
children: [
Text(
dateFormat(
null,
context: context,
stringDate: groupByValue,
ref: ref,
),
],
),
),
itemBuilder: (context, History element) {
final manga = element.chapter.value!.manga.value!;
final chapter = element.chapter.value!;
return ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
elevation: 0,
shadowColor: Colors.transparent,
),
onPressed: () async {
await chapter.pushToReaderView(context);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: SizedBox(
height: 105,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 60,
height: 90,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
),
onPressed: () {
context.push(
'/manga-reader/detail',
extra: manga.id,
);
},
child: ClipRRect(
],
),
),
itemBuilder: (context, History element) {
final chapter = element.chapter.value!;
final manga = chapter.manga.value!;
return ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
backgroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
elevation: 0,
shadowColor: Colors.transparent,
),
onPressed: () async {
await chapter.pushToReaderView(context);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: SizedBox(
height: 105,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 60,
height: 90,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
child: getCoverImage(manga),
),
),
onPressed: () {
context.push(
'/manga-reader/detail',
extra: manga.id,
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(7),
child: _getCoverImage(manga),
),
),
Flexible(
child: Row(
children: [
Expanded(
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
manga.name!,
style: TextStyle(
fontSize: 14,
color: Theme.of(
context,
).textTheme.bodyLarge!.color,
fontWeight: FontWeight.bold,
),
Flexible(
child: Row(
children: [
Expanded(
child: Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
manga.name!,
style: TextStyle(
fontSize: 14,
color: Theme.of(
context,
).textTheme.bodyLarge!.color,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.start,
),
Wrap(
crossAxisAlignment:
WrapCrossAlignment.end,
children: [
Text(
chapter.name!,
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color,
),
),
textAlign: TextAlign.start,
),
Wrap(
crossAxisAlignment:
WrapCrossAlignment.end,
children: [
Text(
chapter.name!,
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color,
),
Text(
" - ${dateFormatHour(element.date!, context)}",
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color,
fontWeight: FontWeight.w400,
),
Text(
" - ${dateFormatHour(element.date!, context)}",
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color,
fontWeight:
FontWeight.w400,
),
),
],
),
],
),
),
],
),
],
),
),
),
IconButton(
onPressed: () => openDeleteDialog(
l10n,
manga,
element.id,
),
icon: Icon(
Icons.delete_outline,
size: 25,
color: Theme.of(
context,
).textTheme.bodyLarge!.color,
),
),
IconButton(
onPressed: () => _openDeleteDialog(
l10n,
manga,
element.id,
),
],
),
icon: Icon(
Icons.delete_outline,
size: 25,
color: Theme.of(
context,
).textTheme.bodyLarge!.color,
),
),
],
),
],
),
),
],
),
),
);
},
itemComparator: (item1, item2) =>
item1.date!.compareTo(item2.date!),
order: GroupedListOrder.DESC,
),
],
);
}
return Center(child: Text(l10n.nothing_read_recently));
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
),
),
);
},
itemComparator: (item1, item2) =>
item1.date!.compareTo(item2.date!),
order: GroupedListOrder.DESC,
),
],
);
}
return Center(child: Text(l10n.nothing_read_recently));
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
);
}
Widget getCoverImage(Manga manga) {
Widget _getCoverImage(Manga manga) {
return manga.customCoverImage != null
? Image.memory(manga.customCoverImage as Uint8List)
: cachedCompressedNetworkImage(
@ -434,7 +414,7 @@ class _HistoryTabState extends ConsumerState<HistoryTab> {
);
}
void openDeleteDialog(AppLocalizations l10n, Manga manga, int? deleteId) {
void _openDeleteDialog(AppLocalizations l10n, Manga manga, int? deleteId) {
showDialog(
context: context,
builder: (context) {

View file

@ -71,6 +71,13 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
}
}
@override
void dispose() {
_textEditingController.dispose();
tabBarController?.dispose();
super.dispose();
}
Future<void> _updateLibrary(List<Manga> mangaList) async {
bool isDark = ref.read(themeModeStateProvider);
botToast(
@ -2078,6 +2085,11 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
void _importLocal(BuildContext context, ItemType itemType) {
final l10n = l10nLocalizations(context)!;
final filesText = switch (itemType) {
ItemType.manga => ".zip, .cbz",
ItemType.anime => ".mp4, .mkv, .avi, and more",
ItemType.novel => ".epub",
};
bool isLoading = false;
showDialog(
context: context,
@ -2126,7 +2138,7 @@ void _importLocal(BuildContext context, ItemType itemType) {
children: [
const Icon(Icons.archive_outlined),
Text(
"${l10n.import_files} ( ${itemType == ItemType.manga ? ".zip, .cbz" : ".mp4, .mkv, .avi, and more"} )",
"${l10n.import_files} ( $filesText )",
style: TextStyle(
color: Theme.of(
context,

View file

@ -1,4 +1,6 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:epubx/epubx.dart';
import 'package:file_picker/file_picker.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
@ -19,9 +21,11 @@ Future importArchivesFromFile(
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: itemType == ItemType.manga
? ['cbz', 'zip']
: ['mp4', 'mov', 'avi', 'flv', 'wmv', 'mpeg', 'mkv'],
allowedExtensions: switch (itemType) {
ItemType.manga => ['cbz', 'zip'],
ItemType.anime => ['mp4', 'mov', 'avi', 'flv', 'wmv', 'mpeg', 'mkv'],
ItemType.novel => ['epub'],
},
);
if (result != null) {
final dateNow = DateTime.now().millisecondsSinceEpoch;
@ -57,16 +61,45 @@ Future importArchivesFromFile(
manga.customCoverImage = itemType == ItemType.manga ? data!.$3 : null;
}
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final chapters = Chapter(
name: itemType == ItemType.manga ? data!.$1 : name,
archivePath: itemType == ItemType.manga ? data!.$4 : file.path,
mangaId: manga.id,
updatedAt: DateTime.now().millisecondsSinceEpoch,
)..manga.value = manga;
isar.chapters.putSync(chapters);
chapters.manga.saveSync();
await isar.writeTxn(() async {
final mangaId = await isar.mangas.put(manga);
final List<Chapter> chapters = [];
if (itemType == ItemType.novel) {
final bytes = await File(file.path!).readAsBytes();
final book = await EpubReader.readBook(bytes);
if (book.Content != null && book.Content!.Images != null) {
final coverImage =
book.Content!.Images!.containsKey("media/file0.png")
? book.Content!.Images!["media/file0.png"]!.Content
: book.Content!.Images!.values.first.Content;
await isar.mangas.put(manga..customCoverImage = coverImage);
}
for (var chapter in book.Chapters ?? []) {
chapters.add(
Chapter(
mangaId: mangaId,
name: chapter.Title is String && chapter.Title.isEmpty
? "Book"
: chapter.Title,
archivePath: file.path,
updatedAt: DateTime.now().millisecondsSinceEpoch,
)..manga.value = manga,
);
}
} else {
chapters.add(
Chapter(
name: itemType == ItemType.manga ? data!.$1 : name,
archivePath: itemType == ItemType.manga ? data!.$4 : file.path,
mangaId: manga.id,
updatedAt: DateTime.now().millisecondsSinceEpoch,
)..manga.value = manga,
);
}
for (final chapter in chapters) {
await isar.chapters.put(chapter);
await chapter.manga.save();
}
});
}
}
@ -80,7 +113,7 @@ String _getName(String path) {
.split("\\")
.last
.replaceAll(
RegExp(r'\.(mp4|mov|avi|flv|wmv|mpeg|mkv|cbz|zip|cbt|tar)'),
RegExp(r'\.(mp4|mov|avi|flv|wmv|mpeg|mkv|cbz|zip|cbt|tar|epub)'),
'',
);
}

View file

@ -7,7 +7,7 @@ part of 'local_archive.dart';
// **************************************************************************
String _$importArchivesFromFileHash() =>
r'e57fafc17833a24bccdd8f945a4c8e6dc50b49c0';
r'4d92aaade0544f76214030364433f91d27570b5a';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -126,34 +126,10 @@ class _LibraryGridViewWidgetState extends State<LibraryGridViewWidget> {
}
},
onLongPress: () {
if (!isLongPressed) {
ref
.read(mangasListStateProvider.notifier)
.update(entry);
ref
.read(isLongPressedMangaStateProvider.notifier)
.update(!isLongPressed);
} else {
ref
.read(mangasListStateProvider.notifier)
.update(entry);
}
_handleLongOrSecondaryTap(isLongPressed, ref, entry);
},
onSecondaryTap: () {
if (!isLongPressed) {
ref
.read(mangasListStateProvider.notifier)
.update(entry);
ref
.read(isLongPressedMangaStateProvider.notifier)
.update(!isLongPressed);
} else {
ref
.read(mangasListStateProvider.notifier)
.update(entry);
}
_handleLongOrSecondaryTap(isLongPressed, ref, entry);
},
children: [
Stack(
@ -446,4 +422,17 @@ class _LibraryGridViewWidgetState extends State<LibraryGridViewWidget> {
},
);
}
void _handleLongOrSecondaryTap(
bool isLongPressed,
WidgetRef ref,
Manga entry,
) {
if (!isLongPressed) {
ref.read(mangasListStateProvider.notifier).update(entry);
ref.read(isLongPressedMangaStateProvider.notifier).update(!isLongPressed);
} else {
ref.read(mangasListStateProvider.notifier).update(entry);
}
}
}

View file

@ -7,6 +7,7 @@ class SeachFormTextField extends StatelessWidget {
final VoidCallback onSuffixPressed;
final TextEditingController controller;
final Function(String)? onFieldSubmitted;
final bool autofocus;
const SeachFormTextField({
super.key,
required this.onChanged,
@ -14,6 +15,7 @@ class SeachFormTextField extends StatelessWidget {
required this.controller,
this.onFieldSubmitted,
required this.onSuffixPressed,
this.autofocus = true,
});
@override
@ -21,7 +23,7 @@ class SeachFormTextField extends StatelessWidget {
final l10n = l10nLocalizations(context)!;
return Flexible(
child: TextFormField(
autofocus: true,
autofocus: autofocus,
controller: controller,
keyboardType: TextInputType.text,
onChanged: onChanged,

View file

@ -89,9 +89,15 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
final offetProvider = StateProvider((ref) => 0.0);
bool _expanded = false;
ScrollController _scrollController = ScrollController();
late final ScrollController _scrollController;
late final isLocalArchive = widget.manga!.isLocalArchive ?? false;
@override
Widget build(BuildContext context) {

View file

@ -24,113 +24,116 @@ class ChapterListTileWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isLongPressed = ref.watch(isLongPressedStateProvider);
final l10n = l10nLocalizations(context)!;
return Container(
color: chapterList.contains(chapter)
? context.primaryColor.withValues(alpha: 0.4)
: null,
child: ListTile(
textColor: chapter.isRead!
? context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3)
: null,
selectedColor: chapter.isRead!
? Colors.white.withValues(alpha: 0.3)
: Colors.white,
onLongPress: () {
if (!isLongPressed) {
ref.read(chaptersListStateProvider.notifier).update(chapter);
ref
.read(isLongPressedStateProvider.notifier)
.update(!isLongPressed);
} else {
ref.read(chaptersListStateProvider.notifier).update(chapter);
}
},
onTap: () async {
if (isLongPressed) {
ref.read(chaptersListStateProvider.notifier).update(chapter);
} else {
chapter.pushToReaderView(context, ignoreIsRead: true);
}
},
title: Row(
children: [
chapter.isBookmarked!
? Icon(Icons.bookmark, size: 16, color: context.primaryColor)
: Container(),
Flexible(child: _buildTitle(chapter.name!, context)),
],
),
subtitle: Row(
children: [
if ((chapter.manga.value!.isLocalArchive ?? false) == false)
Text(
chapter.dateUpload == null || chapter.dateUpload!.isEmpty
? ""
: dateFormat(
chapter.dateUpload!,
ref: ref,
context: context,
child: GestureDetector(
onLongPress: () => _handleInteraction(ref),
onSecondaryTap: () => _handleInteraction(ref),
child: ListTile(
textColor: chapter.isRead!
? context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3)
: null,
selectedColor: chapter.isRead!
? Colors.white.withValues(alpha: 0.3)
: Colors.white,
onTap: () async => _handleInteraction(ref, context),
title: Row(
children: [
chapter.isBookmarked!
? Icon(Icons.bookmark, size: 16, color: context.primaryColor)
: Container(),
Flexible(child: _buildTitle(chapter.name!, context)),
],
),
subtitle: Row(
children: [
if ((chapter.manga.value!.isLocalArchive ?? false) == false)
Text(
chapter.dateUpload == null || chapter.dateUpload!.isEmpty
? ""
: dateFormat(
chapter.dateUpload!,
ref: ref,
context: context,
),
style: const TextStyle(fontSize: 11),
),
if (!chapter.isRead!)
if (chapter.lastPageRead!.isNotEmpty &&
chapter.lastPageRead != "1")
Row(
children: [
const Text(''),
Text(
chapter.manga.value!.itemType == ItemType.anime
? l10n.episode_progress(
Duration(
milliseconds: int.parse(
chapter.lastPageRead!,
),
).toString().substringBefore("."),
)
: l10n.page(
chapter.manga.value!.itemType == ItemType.manga
? chapter.lastPageRead!
: "${((double.tryParse(chapter.lastPageRead!) ?? 0) * 100).toStringAsFixed(0)} %",
),
style: TextStyle(
fontSize: 11,
color: context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3),
),
),
style: const TextStyle(fontSize: 11),
),
if (!chapter.isRead!)
if (chapter.lastPageRead!.isNotEmpty &&
chapter.lastPageRead != "1")
],
),
if (chapter.scanlator?.isNotEmpty ?? false)
Row(
children: [
const Text(''),
Text(
chapter.manga.value!.itemType == ItemType.anime
? l10n.episode_progress(
Duration(
milliseconds: int.parse(chapter.lastPageRead!),
).toString().substringBefore("."),
)
: l10n.page(
chapter.manga.value!.itemType == ItemType.manga
? chapter.lastPageRead!
: "${((double.tryParse(chapter.lastPageRead!) ?? 0) * 100).toStringAsFixed(0)} %",
),
chapter.scanlator!,
style: TextStyle(
fontSize: 11,
color: context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3),
color: chapter.isRead!
? context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3)
: null,
),
),
],
),
if (chapter.scanlator?.isNotEmpty ?? false)
Row(
children: [
const Text(''),
Text(
chapter.scanlator!,
style: TextStyle(
fontSize: 11,
color: chapter.isRead!
? context.isLight
? Colors.black.withValues(alpha: 0.4)
: Colors.white.withValues(alpha: 0.3)
: null,
),
),
],
),
],
],
),
trailing:
!sourceExist || (chapter.manga.value!.isLocalArchive ?? false)
? null
: ChapterPageDownload(chapter: chapter),
),
trailing: !sourceExist || (chapter.manga.value!.isLocalArchive ?? false)
? null
: ChapterPageDownload(chapter: chapter),
),
);
}
void _handleInteraction(WidgetRef ref, [BuildContext? context]) {
final isLongPressed = ref.read(isLongPressedStateProvider);
if (isLongPressed) {
ref.read(chaptersListStateProvider.notifier).update(chapter);
} else {
if (context != null) {
chapter.pushToReaderView(context, ignoreIsRead: true);
} else {
ref.read(chaptersListStateProvider.notifier).update(chapter);
ref.read(isLongPressedStateProvider.notifier).update(!isLongPressed);
}
}
}
Widget _buildTitle(String text, BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {

View file

@ -61,6 +61,12 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
.and()
.itemTypeEqualTo(widget.manga.itemType)
.findAllSync();
@override
void initState() {
super.initState();
final query = widget.manga.name ?? widget.manga.author ?? "";
_textEditingController.text = query;
}
@override
Widget build(BuildContext context) {
@ -68,7 +74,6 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
final query = _query.isNotEmpty
? _query
: widget.manga.name ?? widget.manga.author ?? "";
_textEditingController.text = query;
return Scaffold(
appBar: AppBar(
@ -100,6 +105,7 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
});
},
controller: _textEditingController,
autofocus: false,
),
],
),
@ -126,6 +132,12 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
: Container(),
);
}
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
}
class MigrationSourceSearchScreen extends StatefulWidget {

View file

@ -59,6 +59,13 @@ class _TrackerWidgetSearchState extends ConsumerState<TrackerWidgetSearch> {
}
late final _controller = TextEditingController(text: query);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
bool _isLoading = true;
@override
Widget build(BuildContext context) {

View file

@ -114,6 +114,13 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
return mangaRes;
}
@override
void dispose() {
_scrollController.dispose();
_textEditingController.dispose();
super.dispose();
}
late final _textEditingController = TextEditingController(text: widget.query);
late String _query = widget.query;
late bool _isSearch = widget.isSearch;

View file

@ -182,6 +182,12 @@ class SeachFormTextFieldWidget extends StatefulWidget {
}
class _SeachFormTextFieldWidgetState extends State<SeachFormTextFieldWidget> {
@override
void dispose() {
_controller.dispose();
super.dispose();
}
late final _controller = TextEditingController(text: widget.text);
@override
Widget build(BuildContext context) {

View file

@ -83,6 +83,12 @@ class _DoubleColummViewState extends State<DoubleColummView>
});
}
@override
void dispose() {
_scaleAnimationController.dispose();
super.dispose();
}
void _toggleScale(Offset tapPosition) {
if (mounted) {
setState(() {

View file

@ -126,7 +126,7 @@ class MangaChapterPageGallery extends ConsumerStatefulWidget {
class _MangaChapterPageGalleryState
extends ConsumerState<MangaChapterPageGallery>
with TickerProviderStateMixin {
with TickerProviderStateMixin, WidgetsBindingObserver {
late AnimationController _scaleAnimationController;
late Animation<double> _animation;
late ReaderController _readerController = ref.read(
@ -136,6 +136,7 @@ class _MangaChapterPageGalleryState
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_readerController.setMangaHistoryUpdate();
final index = _uChapDataPreload[_currentIndex!].index;
if (index != null) {
@ -144,7 +145,15 @@ class _MangaChapterPageGalleryState
_rebuildDetail.close();
_doubleClickAnimationController.dispose();
_scaleAnimationController.dispose();
_failedToLoadImage.dispose();
_autoScroll.value = false;
_autoScroll.dispose();
_autoScrollPage.dispose();
_itemPositionsListener.itemPositions.removeListener(_readProgressListener);
_photoViewController.dispose();
_photoViewScaleStateController.dispose();
_extendedController.dispose();
clearGestureDetailsCache();
if (isDesktop) {
setFullScreen(value: false);
@ -158,6 +167,17 @@ class _MangaChapterPageGalleryState
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
final index = _uChapDataPreload[_currentIndex!].index;
if (index != null) {
_readerController.setPageIndex(_geCurrentIndex(index), true);
}
}
}
late final _autoScroll = ValueNotifier(
_readerController.autoScrollValues().$1,
);
@ -203,6 +223,7 @@ class _MangaChapterPageGalleryState
_itemPositionsListener.itemPositions.addListener(_readProgressListener);
_initCurrentIndex();
discordRpc.showChapterDetails(ref, chapter);
WidgetsBinding.instance.addObserver(this);
}
final double _horizontalScaleValue = 1.0;

View file

@ -60,6 +60,12 @@ class _ChapterListWidgetState extends State<ChapterListWidget> {
_jumpTo();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
Future<void> _jumpTo() async {
await Future.delayed(const Duration(milliseconds: 5));
controller.jumpTo(

View file

@ -29,6 +29,12 @@ class _DownloadFileScreenState extends ConsumerState<DownloadFileScreen> {
final List<int> _bytes = [];
late StreamSubscription<List<int>>? _subscription;
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context)!;

View file

@ -24,33 +24,38 @@ class CategoriesScreen extends ConsumerStatefulWidget {
class _CategoriesScreenState extends ConsumerState<CategoriesScreen>
with TickerProviderStateMixin {
late TabController _tabBarController;
int tabs = 3;
late final List<String> _tabList;
@override
void initState() {
super.initState();
_tabBarController = TabController(length: tabs, vsync: this);
final hideItems = ref.read(hideItemsStateProvider);
_tabList = [
if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary",
if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary",
if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary",
];
_tabBarController = TabController(length: _tabList.length, vsync: this);
_tabBarController.animateTo(widget.data.$2);
}
@override
void dispose() {
_tabBarController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
int newTabs = 0;
final hideItems = ref.watch(hideItemsStateProvider);
if (!hideItems.contains("/MangaLibrary")) newTabs++;
if (!hideItems.contains("/AnimeLibrary")) newTabs++;
if (!hideItems.contains("/NovelLibrary")) newTabs++;
if (tabs != newTabs) {
_tabBarController.dispose();
_tabBarController = TabController(length: newTabs, vsync: this);
_tabBarController.animateTo(0);
setState(() {
tabs = newTabs;
});
if (_tabList.isEmpty) {
return Scaffold(
appBar: AppBar(title: Text(context.l10n.categories)),
body: Center(child: Text("EMPTY\nMPTY\nMTY\nMT\n\n")),
);
}
final l10n = l10nLocalizations(context)!;
return DefaultTabController(
animationDuration: Duration.zero,
length: newTabs,
length: _tabList.length,
child: Scaffold(
appBar: AppBar(
elevation: 0,
@ -62,23 +67,24 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen>
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.label,
controller: _tabBarController,
tabs: [
if (!hideItems.contains("/MangaLibrary")) Tab(text: l10n.manga),
if (!hideItems.contains("/AnimeLibrary")) Tab(text: l10n.anime),
if (!hideItems.contains("/NovelLibrary")) Tab(text: l10n.novel),
],
tabs: _tabList.map((route) {
if (route == "/MangaLibrary") return Tab(text: l10n.manga);
if (route == "/AnimeLibrary") return Tab(text: l10n.anime);
return Tab(text: l10n.novel);
}).toList(),
),
),
body: TabBarView(
controller: _tabBarController,
children: [
if (!hideItems.contains("/MangaLibrary"))
CategoriesTab(itemType: ItemType.manga),
if (!hideItems.contains("/AnimeLibrary"))
CategoriesTab(itemType: ItemType.anime),
if (!hideItems.contains("/NovelLibrary"))
CategoriesTab(itemType: ItemType.novel),
],
children: _tabList.map((route) {
if (route == "/MangaLibrary") {
return CategoriesTab(itemType: ItemType.manga);
}
if (route == "/AnimeLibrary") {
return CategoriesTab(itemType: ItemType.anime);
}
return CategoriesTab(itemType: ItemType.novel);
}).toList(),
),
),
);

View file

@ -24,7 +24,12 @@ class _TrackingDetailState extends State<TrackingDetail>
void initState() {
super.initState();
_tabBarController = TabController(length: 2, vsync: this);
_tabBarController.animateTo(0);
}
@override
void dispose() {
_tabBarController.dispose();
super.dispose();
}
@override

View file

@ -15,17 +15,19 @@ class StatisticsScreen extends ConsumerStatefulWidget {
class _StatisticsScreenState extends ConsumerState<StatisticsScreen>
with SingleTickerProviderStateMixin {
late final hideItems = ref.read(hideItemsStateProvider);
late final List<String> hideItems;
late TabController _tabController;
late final List<String> _tabList;
late final _tabList = [
if (!hideItems.contains("/MangaLibrary")) 'manga',
if (!hideItems.contains("/AnimeLibrary")) 'anime',
if (!hideItems.contains("/NovelLibrary")) 'novel',
];
@override
void didChangeDependencies() {
super.didChangeDependencies();
void initState() {
super.initState();
hideItems = ref.read(hideItemsStateProvider);
_tabList = [
if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary",
if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary",
if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary",
];
_tabController = TabController(length: _tabList.length, vsync: this);
}
@ -38,7 +40,10 @@ class _StatisticsScreenState extends ConsumerState<StatisticsScreen>
@override
Widget build(BuildContext context) {
if (_tabList.isEmpty) {
return SizedBox.shrink();
return Scaffold(
appBar: AppBar(title: Text(context.l10n.statistics)),
body: Center(child: Text("EMPTY\nMPTY\nMTY\nMT\n\n")),
);
}
final l10n = context.l10n;
return Scaffold(
@ -46,23 +51,24 @@ class _StatisticsScreenState extends ConsumerState<StatisticsScreen>
title: Text(l10n.statistics),
bottom: TabBar(
controller: _tabController,
tabs: [
if (!hideItems.contains("/MangaLibrary")) Tab(text: "Manga"),
if (!hideItems.contains("/AnimeLibrary")) Tab(text: "Anime"),
if (!hideItems.contains("/NovelLibrary")) Tab(text: "Novel"),
],
tabs: _tabList.map((route) {
if (route == "/MangaLibrary") return Tab(text: l10n.manga);
if (route == "/AnimeLibrary") return Tab(text: l10n.anime);
return Tab(text: l10n.novel);
}).toList(),
),
),
body: TabBarView(
controller: _tabController,
children: [
if (!hideItems.contains("/MangaLibrary"))
_buildStatisticsTab(itemType: ItemType.manga),
if (!hideItems.contains("/AnimeLibrary"))
_buildStatisticsTab(itemType: ItemType.anime),
if (!hideItems.contains("/NovelLibrary"))
_buildStatisticsTab(itemType: ItemType.novel),
],
children: _tabList.map((route) {
if (route == "/MangaLibrary") {
return _buildStatisticsTab(itemType: ItemType.manga);
}
if (route == "/AnimeLibrary") {
return _buildStatisticsTab(itemType: ItemType.anime);
}
return _buildStatisticsTab(itemType: ItemType.novel);
}).toList(),
),
);
}

View file

@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:epubx/epubx.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_qjs/quickjs/ffi.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@ -17,6 +19,7 @@ import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_pr
import 'package:mangayomi/modules/novel/novel_reader_controller_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/get_html_content.dart';
import 'package:mangayomi/utils/extensions/dom_extensions.dart';
import 'package:mangayomi/utils/utils.dart';
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:mangayomi/services/get_chapter_pages.dart';
@ -25,6 +28,8 @@ import 'package:mangayomi/utils/global_style.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:html/dom.dart' as dom;
import 'package:flutter/widgets.dart' as widgets;
typedef DoubleClickAnimationListener = void Function();
@ -34,21 +39,17 @@ class NovelReaderView extends ConsumerWidget {
late final Chapter chapter = isar.chapters.getSync(chapterId)!;
@override
Widget build(BuildContext context, WidgetRef ref) {
final htmlContent = ref.watch(getHtmlContentProvider(chapter: chapter));
final result = ref.watch(getHtmlContentProvider(chapter: chapter));
return NovelWebView(chapter: chapter, htmlContent: htmlContent);
return NovelWebView(chapter: chapter, result: result);
}
}
class NovelWebView extends ConsumerStatefulWidget {
const NovelWebView({
super.key,
required this.chapter,
required this.htmlContent,
});
const NovelWebView({super.key, required this.chapter, required this.result});
final Chapter chapter;
final AsyncValue<String> htmlContent;
final AsyncValue<(String, EpubBook?)> result;
@override
ConsumerState createState() {
@ -99,6 +100,7 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
}
late Chapter chapter = widget.chapter;
EpubBook? epubBook;
final StreamController<double> _rebuildDetail =
StreamController<double>.broadcast();
@ -206,8 +208,9 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
children: [
Row(
children: [
widget.htmlContent.when(
data: (htmlContent) {
widget.result.when(
data: (data) {
epubBook = data.$2;
Future.delayed(const Duration(milliseconds: 1000), () {
if (!scrolled && _scrollController.hasClients) {
_scrollController.animateTo(
@ -223,48 +226,48 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
child: Scrollbar(
controller: _scrollController,
interactive: true,
child: SingleChildScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_isViewFunction();
},
child: Column(
children: [
HtmlWidget(
htmlContent,
customStylesBuilder: (element) {
switch (backgroundColor) {
case BackgroundColor.black:
return {
'background-color': 'black',
};
default:
return {
'background-color': '#F0F0F0',
};
}
},
onTapUrl: (url) {
context.push(
"/mangawebview",
extra: {'url': url, 'title': url},
);
return true;
},
renderMode: RenderMode.column,
textStyle: TextStyle(
color:
backgroundColor ==
BackgroundColor.white
? Colors.black
: Colors.white,
fontSize: fontSize.toDouble(),
),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_isViewFunction();
},
child: CustomScrollView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
slivers: [
HtmlWidget(
data.$1,
customWidgetBuilder: (element) =>
_buildCustomWidgets(element),
customStylesBuilder: (element) {
switch (backgroundColor) {
case BackgroundColor.black:
return {'background-color': 'black'};
default:
return {
'background-color': '#F0F0F0',
};
}
},
onTapUrl: (url) {
context.push(
"/mangawebview",
extra: {'url': url, 'title': url},
);
return true;
},
renderMode: RenderMode.sliverList,
textStyle: TextStyle(
color:
backgroundColor ==
BackgroundColor.white
? Colors.black
: Colors.white,
fontSize: fontSize.toDouble(),
),
Center(
),
SliverToBoxAdapter(
child: Center(
heightFactor: 2,
child: Row(
mainAxisAlignment:
@ -310,8 +313,8 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
],
),
),
],
),
),
],
),
),
),
@ -735,6 +738,26 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
}
}
}
Widget? _buildCustomWidgets(dom.Element element) {
if (element.localName == "img" &&
element.getSrc != null &&
epubBook != null) {
final fileName = element.getSrc!.split("/").last;
final image = epubBook!.Content!.Images!.entries
.firstWhereOrNull((img) => img.key.endsWith(fileName))
?.value
.Content;
return image != null
? widgets.Image(
errorBuilder: (context, error, stackTrace) => Text(""),
fit: BoxFit.scaleDown,
image: MemoryImage(image as Uint8List) as ImageProvider,
)
: null;
}
return null;
}
}
class UChapDataPreload {

View file

@ -48,6 +48,12 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
List<TrackLibrarySection> _sections = [];
List<TrackPreference> _preferences = [];
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context)!;

View file

@ -11,7 +11,6 @@ import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/update.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
@ -34,8 +33,9 @@ class UpdatesScreen extends ConsumerStatefulWidget {
class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
with TickerProviderStateMixin {
late TabController _tabBarController;
late final List<String> _tabList;
late final List<String> hideItems;
bool _isLoading = false;
int tabs = 3;
Future<void> _updateLibrary() async {
setState(() {
_isLoading = true;
@ -92,13 +92,6 @@ class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
);
}
}
await Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
if (mangaList.length == numbers) {
return false;
}
return true;
});
BotToast.cleanAll();
setState(() {
_isLoading = false;
@ -112,213 +105,201 @@ class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
});
}
@override
void dispose() {
_textEditingController.dispose();
_tabBarController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
_tabBarController = TabController(length: tabs, vsync: this);
_tabBarController.animateTo(0);
hideItems = ref.read(hideItemsStateProvider);
_tabList = [
if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary",
if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary",
if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary",
];
_tabBarController = TabController(length: _tabList.length, vsync: this);
_tabBarController.addListener(tabListener);
}
final _textEditingController = TextEditingController();
bool _isSearch = false;
List<History> entriesData = [];
@override
Widget build(BuildContext context) {
int newTabs = 0;
final hideItems = ref.watch(hideItemsStateProvider);
if (!hideItems.contains("/MangaLibrary")) newTabs++;
if (!hideItems.contains("/AnimeLibrary")) newTabs++;
if (!hideItems.contains("/NovelLibrary")) newTabs++;
if (newTabs == 0) {
return SizedBox.shrink();
}
if (tabs != newTabs) {
_tabBarController.removeListener(tabListener);
_tabBarController.dispose();
_tabBarController = TabController(length: newTabs, vsync: this);
_tabBarController.animateTo(0);
_tabBarController.addListener(tabListener);
setState(() {
tabs = newTabs;
});
}
final l10n = l10nLocalizations(context)!;
return DefaultTabController(
animationDuration: Duration.zero,
length: newTabs,
child: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
title: _isSearch
? null
: Text(
l10n.updates,
style: TextStyle(color: Theme.of(context).hintColor),
),
actions: [
_isSearch
? SeachFormTextField(
onChanged: (value) {
setState(() {});
},
onSuffixPressed: () {
_textEditingController.clear();
setState(() {});
},
onPressed: () {
setState(() {
_isSearch = false;
});
_textEditingController.clear();
},
controller: _textEditingController,
)
: IconButton(
splashRadius: 20,
onPressed: () {
setState(() {
_isSearch = true;
});
},
icon: Icon(
Icons.search_outlined,
color: Theme.of(context).hintColor,
),
),
IconButton(
splashRadius: 20,
onPressed: () {
_updateLibrary();
},
icon: Icon(
Icons.refresh_outlined,
color: Theme.of(context).hintColor,
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
title: _isSearch
? null
: Text(
l10n.updates,
style: TextStyle(color: Theme.of(context).hintColor),
),
),
IconButton(
splashRadius: 20,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.remove_everything),
content: Text(l10n.remove_all_update_msg),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () => clearUpdates(hideItems, context),
child: Text(l10n.ok),
),
],
),
],
);
actions: [
_isSearch
? SeachFormTextField(
onChanged: (value) {
setState(() {});
},
);
},
icon: Icon(
Icons.delete_sweep_outlined,
color: Theme.of(context).hintColor,
),
onSuffixPressed: () {
_textEditingController.clear();
setState(() {});
},
onPressed: () {
setState(() {
_isSearch = false;
});
_textEditingController.clear();
},
controller: _textEditingController,
)
: IconButton(
splashRadius: 20,
onPressed: () {
setState(() {
_isSearch = true;
});
},
icon: Icon(
Icons.search_outlined,
color: Theme.of(context).hintColor,
),
),
IconButton(
splashRadius: 20,
onPressed: () {
_updateLibrary();
},
icon: Icon(
Icons.refresh_outlined,
color: Theme.of(context).hintColor,
),
),
IconButton(
splashRadius: 20,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.remove_everything),
content: Text(l10n.remove_all_update_msg),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () async {
if (mounted) Navigator.pop(context);
await _clearUpdates(hideItems);
},
child: Text(l10n.ok),
),
],
),
],
);
},
);
},
icon: Icon(
Icons.delete_sweep_outlined,
color: Theme.of(context).hintColor,
),
),
],
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
controller: _tabBarController,
tabs: [
if (!hideItems.contains("/MangaLibrary"))
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Tab(text: l10n.manga),
const SizedBox(width: 8),
_updateNumbers(ref, ItemType.manga),
],
),
if (!hideItems.contains("/AnimeLibrary"))
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Tab(text: l10n.anime),
const SizedBox(width: 8),
_updateNumbers(ref, ItemType.anime),
],
),
if (!hideItems.contains("/NovelLibrary"))
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Tab(text: l10n.novel),
const SizedBox(width: 8),
_updateNumbers(ref, ItemType.novel),
],
),
],
bottom: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
controller: _tabBarController,
tabs: [
if (!hideItems.contains("/MangaLibrary"))
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Tab(text: l10n.manga),
const SizedBox(width: 8),
_updateNumbers(ref, ItemType.manga),
],
),
if (!hideItems.contains("/AnimeLibrary"))
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Tab(text: l10n.anime),
const SizedBox(width: 8),
_updateNumbers(ref, ItemType.anime),
],
),
if (!hideItems.contains("/NovelLibrary"))
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Tab(text: l10n.novel),
const SizedBox(width: 8),
_updateNumbers(ref, ItemType.novel),
],
),
],
),
),
body: Padding(
padding: const EdgeInsets.only(top: 10),
child: TabBarView(
controller: _tabBarController,
children: [
if (!hideItems.contains("/MangaLibrary"))
UpdateTab(
itemType: ItemType.manga,
query: _textEditingController.text,
isLoading: _isLoading,
),
if (!hideItems.contains("/AnimeLibrary"))
UpdateTab(
itemType: ItemType.anime,
query: _textEditingController.text,
isLoading: _isLoading,
),
if (!hideItems.contains("/NovelLibrary"))
UpdateTab(
itemType: ItemType.novel,
query: _textEditingController.text,
isLoading: _isLoading,
),
],
),
),
body: Padding(
padding: const EdgeInsets.only(top: 10),
child: TabBarView(
controller: _tabBarController,
children: [
if (!hideItems.contains("/MangaLibrary"))
UpdateTab(
itemType: ItemType.manga,
query: _textEditingController.text,
isLoading: _isLoading,
),
if (!hideItems.contains("/AnimeLibrary"))
UpdateTab(
itemType: ItemType.anime,
query: _textEditingController.text,
isLoading: _isLoading,
),
if (!hideItems.contains("/NovelLibrary"))
UpdateTab(
itemType: ItemType.novel,
query: _textEditingController.text,
isLoading: _isLoading,
),
],
),
),
);
}
void clearUpdates(List<String> hideItems, BuildContext context) {
List<Update> updates = isar.updates
Future<void> _clearUpdates(List<String> hideItems) async {
List<Update> updates = await isar.updates
.filter()
.idIsNotNull()
.chapter(
(q) =>
q.manga((q) => q.itemTypeEqualTo(getCurrentItemType(hideItems))),
)
.findAllSync()
.toList();
isar.writeTxnSync(() {
for (var update in updates) {
isar.updates.deleteSync(update.id!);
ref
.read(synchingProvider(syncId: 1).notifier)
.addChangedPart(ActionType.removeUpdate, update.id, "{}", false);
}
});
if (mounted) {
Navigator.pop(context);
.findAll();
final idsToDelete = <Id>[];
for (var update in updates) {
idsToDelete.add(update.id!);
ref
.read(synchingProvider(syncId: 1).notifier)
.addChangedPart(ActionType.removeUpdate, update.id, "{}", false);
}
await isar.writeTxn(() => isar.updates.deleteAll(idsToDelete));
}
ItemType getCurrentItemType(List<String> hideItems) {
@ -347,9 +328,14 @@ class UpdateTab extends ConsumerStatefulWidget {
ConsumerState<UpdateTab> createState() => _UpdateTabState();
}
class _UpdateTabState extends ConsumerState<UpdateTab> {
class _UpdateTabState extends ConsumerState<UpdateTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
final l10n = l10nLocalizations(context)!;
final update = ref.watch(
getAllUpdateStreamProvider(
@ -357,105 +343,103 @@ class _UpdateTabState extends ConsumerState<UpdateTab> {
search: widget.query,
),
);
return Scaffold(
body: Stack(
children: [
update.when(
data: (entries) {
final lastUpdatedList = entries
.map((e) => e.chapter.value!.manga.value!.lastUpdate!)
.toList();
lastUpdatedList.sort((a, b) => b.compareTo(a));
final lastUpdated = lastUpdatedList.firstOrNull;
if (entries.isNotEmpty) {
return CustomScrollView(
slivers: [
if (lastUpdated != null)
SliverPadding(
padding: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
bottom: 20,
),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed([
Text(
l10n.library_last_updated(
dateFormat(
lastUpdated.toString(),
ref: ref,
context: context,
showHOURorMINUTE: true,
),
),
style: TextStyle(
fontStyle: FontStyle.italic,
color: context.secondaryColor,
),
),
]),
),
return Stack(
children: [
update.when(
data: (entries) {
final lastUpdatedList = entries
.map((e) => e.chapter.value!.manga.value!.lastUpdate!)
.toList();
lastUpdatedList.sort((a, b) => b.compareTo(a));
final lastUpdated = lastUpdatedList.firstOrNull;
if (entries.isNotEmpty) {
return CustomScrollView(
slivers: [
if (lastUpdated != null)
SliverPadding(
padding: const EdgeInsets.only(
left: 10,
right: 10,
top: 10,
bottom: 20,
),
CustomSliverGroupedListView<Update, String>(
elements: entries,
groupBy: (element) => dateFormat(
element.date!,
context: context,
ref: ref,
forHistoryValue: true,
useRelativeTimesTamps: false,
),
groupSeparatorBuilder: (String groupByValue) => Padding(
padding: const EdgeInsets.only(bottom: 8, left: 12),
child: Row(
children: [
Text(
sliver: SliverList(
delegate: SliverChildListDelegate.fixed([
Text(
l10n.library_last_updated(
dateFormat(
null,
context: context,
stringDate: groupByValue,
lastUpdated.toString(),
ref: ref,
context: context,
showHOURorMINUTE: true,
),
),
],
),
style: TextStyle(
fontStyle: FontStyle.italic,
color: context.secondaryColor,
),
),
]),
),
itemBuilder: (context, element) {
final chapter = element.chapter.value!;
return UpdateChapterListTileWidget(
chapter: chapter,
sourceExist: true,
);
},
itemComparator: (item1, item2) =>
item1.date!.compareTo(item2.date!),
order: GroupedListOrder.DESC,
),
],
);
}
return Center(child: Text(l10n.no_recent_updates));
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
),
if (widget.isLoading)
const Positioned(
top: 0,
left: 0,
right: 0,
child: Padding(
padding: EdgeInsets.only(top: 40),
child: Center(child: RefreshProgressIndicator()),
),
CustomSliverGroupedListView<Update, String>(
elements: entries,
groupBy: (element) => dateFormat(
element.date!,
context: context,
ref: ref,
forHistoryValue: true,
useRelativeTimesTamps: false,
),
groupSeparatorBuilder: (String groupByValue) => Padding(
padding: const EdgeInsets.only(bottom: 8, left: 12),
child: Row(
children: [
Text(
dateFormat(
null,
context: context,
stringDate: groupByValue,
ref: ref,
),
),
],
),
),
itemBuilder: (context, element) {
final chapter = element.chapter.value!;
return UpdateChapterListTileWidget(
chapter: chapter,
sourceExist: true,
);
},
itemComparator: (item1, item2) =>
item1.date!.compareTo(item2.date!),
order: GroupedListOrder.DESC,
),
],
);
}
return Center(child: Text(l10n.no_recent_updates));
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
),
if (widget.isLoading)
const Positioned(
top: 0,
left: 0,
right: 0,
child: Padding(
padding: EdgeInsets.only(top: 40),
child: Center(child: RefreshProgressIndicator()),
),
],
),
),
],
);
}
}

View file

@ -38,6 +38,17 @@ class _MangaWebViewState extends ConsumerState<MangaWebView> {
}
}
@override
void dispose() {
if (Platform.isLinux) {
_desktopWebview?.close();
} else {
if (browser.isOpened()) browser.close();
browser.dispose();
}
super.dispose();
}
Webview? _desktopWebview;
_runWebViewDesktop() async {
if (Platform.isLinux) {

View file

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:math';
import 'package:draggable_menu/draggable_menu.dart';
import 'package:flutter/material.dart';
@ -64,10 +65,8 @@ Future<void> customDraggableTabBar({
index = tabBarController.index;
if (index != currentIndex) {
index = currentIndex;
refresh();
} else {
refresh();
}
refresh();
});
await showDialog(
@ -79,10 +78,7 @@ Future<void> customDraggableTabBar({
for (var i = 0; i < children.length; i++) ...[
MeasureWidgetSize(
onCalculateSize: (size) {
final additionnalHeight = ((List.generate(
10000,
(index) => index * 0.0001,
))..shuffle()).first;
final additionnalHeight = Random().nextDouble() * 0.01;
double newHeight = size!.height + 52.0 + additionnalHeight;
if (!(newHeight <= maxHeight)) {
newHeight = maxHeight + additionnalHeight;
@ -212,4 +208,5 @@ Future<void> customDraggableTabBar({
),
);
}
tabBarController.dispose();
}

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:epubx/epubx.dart';
import 'package:html/parser.dart';
import 'package:mangayomi/eval/lib.dart';
import 'package:mangayomi/models/chapter.dart';
@ -10,10 +11,27 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
part 'get_html_content.g.dart';
@riverpod
Future<String> getHtmlContent(Ref ref, {required Chapter chapter}) async {
Future<(String, EpubBook?)> getHtmlContent(
Ref ref, {
required Chapter chapter,
}) async {
if (!chapter.manga.isLoaded) {
chapter.manga.loadSync();
}
if (chapter.archivePath != null && chapter.archivePath!.isNotEmpty) {
final htmlFile = File(chapter.archivePath!);
if (await htmlFile.exists()) {
final bytes = await htmlFile.readAsBytes();
final book = await EpubReader.readBook(bytes);
final tempChapter = book.Chapters?.where(
(element) => element.Title!.isNotEmpty
? element.Title == chapter.name
: "Book" == chapter.name,
).firstOrNull;
return (_buildHtml(tempChapter?.HtmlContent ?? "No content"), book);
}
return (_buildHtml("Local epub file not found!"), null);
}
final storageProvider = StorageProvider();
final mangaDirectory = await storageProvider.getMangaMainDirectory(chapter);
final htmlPath = "${mangaDirectory!.path}${chapter.name}.html";
@ -37,7 +55,11 @@ Future<String> getHtmlContent(Ref ref, {required Chapter chapter}) async {
source!,
).getHtmlContent(chapter.manga.value!.name!, chapter.url!);
}
return '''<div id="readerViewContent"><div style="padding: 2em;">${html.substring(1, html.length - 1)}</div></div>'''
return (_buildHtml(html.substring(1, html.length - 1)), null);
}
String _buildHtml(String input) {
return '''<div id="readerViewContent"><div style="padding: 2em;">$input</div></div>'''
.replaceAll("\\n", "")
.replaceAll("\\t", "")
.replaceAll("\\\"", "\"");

View file

@ -6,7 +6,7 @@ part of 'get_html_content.dart';
// RiverpodGenerator
// **************************************************************************
String _$getHtmlContentHash() => r'6bdc17222f959cb5f91b56027d4f98e26571175d';
String _$getHtmlContentHash() => r'19e6959d8fceb065b19c6c6d38cd1b5132a8ba94';
/// Copied from Dart SDK
class _SystemHash {
@ -34,7 +34,7 @@ class _SystemHash {
const getHtmlContentProvider = GetHtmlContentFamily();
/// See also [getHtmlContent].
class GetHtmlContentFamily extends Family<AsyncValue<String>> {
class GetHtmlContentFamily extends Family<AsyncValue<(String, EpubBook?)>> {
/// See also [getHtmlContent].
const GetHtmlContentFamily();
@ -72,7 +72,8 @@ class GetHtmlContentFamily extends Family<AsyncValue<String>> {
}
/// See also [getHtmlContent].
class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
class GetHtmlContentProvider
extends AutoDisposeFutureProvider<(String, EpubBook?)> {
/// See also [getHtmlContent].
GetHtmlContentProvider({
required Chapter chapter,
@ -107,7 +108,7 @@ class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
@override
Override overrideWith(
FutureOr<String> Function(GetHtmlContentRef provider) create,
FutureOr<(String, EpubBook?)> Function(GetHtmlContentRef provider) create,
) {
return ProviderOverride(
origin: this,
@ -124,7 +125,7 @@ class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
}
@override
AutoDisposeFutureProviderElement<String> createElement() {
AutoDisposeFutureProviderElement<(String, EpubBook?)> createElement() {
return _GetHtmlContentProviderElement(this);
}
@ -144,13 +145,14 @@ class GetHtmlContentProvider extends AutoDisposeFutureProvider<String> {
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin GetHtmlContentRef on AutoDisposeFutureProviderRef<String> {
mixin GetHtmlContentRef on AutoDisposeFutureProviderRef<(String, EpubBook?)> {
/// The parameter `chapter` of this provider.
Chapter get chapter;
}
class _GetHtmlContentProviderElement
extends AutoDisposeFutureProviderElement<String> with GetHtmlContentRef {
extends AutoDisposeFutureProviderElement<(String, EpubBook?)>
with GetHtmlContentRef {
_GetHtmlContentProviderElement(super.provider);
@override

View file

@ -146,7 +146,7 @@ class MyAnimeList extends _$MyAnimeList {
publishingType: res["media_type"].toString().replaceAll("_", " "),
publishingStatus: res["status"].toString().replaceAll("_", " "),
trackingUrl: "https://myanimelist.net/$item/${res["id"]}",
score: res["mean"],
score: (res["mean"] as num?)?.toDouble(),
syncId: syncId,
);
}
@ -171,32 +171,23 @@ class MyAnimeList extends _$MyAnimeList {
return res['data'] == null
? []
: (res['data'] as List)
.map(
(e) => TrackSearch(
mediaId: e["node"]["id"],
summary: e["node"]["synopsis"] ?? "",
totalChapter: e["node"][contentUnit],
coverUrl: e["node"]["main_picture"]["large"] ?? "",
title: e["node"]["title"],
score: e["node"]["mean"] is double
? e["node"]["mean"]
: ((e["node"]["mean"] ?? 0) as int).toDouble(),
startDate: e["node"]["start_date"] ?? "",
publishingType: e["node"]["media_type"].toString().replaceAll(
"_",
" ",
),
publishingStatus: e["node"]["status"].toString().replaceAll(
"_",
" ",
),
trackingUrl:
"https://myanimelist.net/$item/${e["node"]["id"]}",
syncId: syncId,
),
)
.toList();
: (res['data'] as List).map((e) {
final node = e["node"] as Map<String, dynamic>;
String clean(String? s) => (s ?? '').replaceAll('_', ' ');
return TrackSearch(
mediaId: node["id"],
summary: node["synopsis"] ?? "",
totalChapter: node[contentUnit],
coverUrl: node["main_picture"]["large"] ?? "",
title: node["title"],
score: (node["mean"] as num?)?.toDouble(),
startDate: node["start_date"] ?? "",
publishingType: clean(node["media_type"].toString()),
publishingStatus: clean(node["status"].toString()),
trackingUrl: "https://myanimelist.net/$item/${node["id"]}",
syncId: syncId,
);
}).toList();
}
Future<List<TrackSearch>> fetchUserData({bool isManga = true}) async {
@ -219,46 +210,31 @@ class MyAnimeList extends _$MyAnimeList {
return res['data'] == null
? []
: (res['data'] as List)
.map(
(e) => TrackSearch(
mediaId: e["node"]["id"],
summary: e["node"]["synopsis"] ?? "",
totalChapter: e["node"][contentUnit],
coverUrl: e["node"]["main_picture"]["large"] ?? "",
title: e["node"]["title"],
score: e["node"]["mean"] is double
? e["node"]["mean"]
: ((e["node"]["mean"] ?? 0) as int).toDouble(),
startDate: e["node"]["start_date"] ?? "",
publishingType: e["node"]["media_type"].toString().replaceAll(
"_",
" ",
),
publishingStatus: e["node"]["status"].toString().replaceAll(
"_",
" ",
),
trackingUrl:
"https://myanimelist.net/$item/${e["node"]["id"]}",
startedReadingDate: _parseDate(
e["list_status"]["start_date"],
),
finishedReadingDate: _parseDate(
e["list_status"]["finish_date"],
),
lastChapterRead:
e["list_status"][isManga
? "num_chapters_read"
: "num_episodes_watched"],
status: fromMyAnimeListStatus(
e["list_status"]["status"],
isManga,
).name,
syncId: syncId,
),
)
.toList();
: (res['data'] as List).map((e) {
final node = e["node"] as Map<String, dynamic>;
final listStatus = e["list_status"] as Map<String, dynamic>;
String clean(String? s) => (s ?? '').replaceAll('_', ' ');
return TrackSearch(
mediaId: node["id"],
summary: node["synopsis"] ?? "",
totalChapter: node[contentUnit],
coverUrl: node["main_picture"]["large"] ?? "",
title: node["title"],
score: (node["mean"] as num?)?.toDouble(),
startDate: node["start_date"] ?? "",
publishingType: clean(node["media_type"].toString()),
publishingStatus: clean(node["status"].toString()),
trackingUrl: "https://myanimelist.net/$item/${node["id"]}",
startedReadingDate: _parseDate(listStatus["start_date"]),
finishedReadingDate: _parseDate(listStatus["finish_date"]),
lastChapterRead:
listStatus[isManga
? "num_chapters_read"
: "num_episodes_watched"],
status: fromMyAnimeListStatus(listStatus["status"], isManga).name,
syncId: syncId,
);
}).toList();
}
String _convertToIsoDate(int? epochTime) {

View file

@ -6,7 +6,7 @@ part of 'myanimelist.dart';
// RiverpodGenerator
// **************************************************************************
String _$myAnimeListHash() => r'8e21378e2a3ccdf696a0a1c9a88a05123f65eacb';
String _$myAnimeListHash() => r'a612e9ce814268ac79dc86d810ca6bd3671812e6';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -0,0 +1 @@
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png

View file

@ -0,0 +1 @@
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png

View file

@ -0,0 +1 @@
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png

View file

@ -0,0 +1 @@
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png

View file

@ -0,0 +1 @@
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png

View file

@ -0,0 +1 @@
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png