mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
Merge branch 'main' into feature/discord-rpc
This commit is contained in:
commit
e290e0ad4a
48 changed files with 2373 additions and 1334 deletions
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
|
|
@ -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
362
go/build_go.sh
Executable 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
112
go/go.mod
|
|
@ -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
301
go/go.sum
|
|
@ -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=
|
||||
|
|
|
|||
797
go/server.go
797
go/server.go
|
|
@ -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, <Res)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (_) {}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)!;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)'),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'local_archive.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$importArchivesFromFileHash() =>
|
||||
r'e57fafc17833a24bccdd8f945a4c8e6dc50b49c0';
|
||||
r'4d92aaade0544f76214030364433f91d27570b5a';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ class _DoubleColummViewState extends State<DoubleColummView>
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scaleAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleScale(Offset tapPosition) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)!;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)!;
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("\\\"", "\"");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'myanimelist.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$myAnimeListHash() => r'8e21378e2a3ccdf696a0a1c9a88a05123f65eacb';
|
||||
String _$myAnimeListHash() => r'a612e9ce814268ac79dc86d810ca6bd3671812e6';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
1
linux/packaging/icons/hicolor/128x128/apps/mangayomi.png
Symbolic link
1
linux/packaging/icons/hicolor/128x128/apps/mangayomi.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
|
||||
1
linux/packaging/icons/hicolor/16x16/apps/mangayomi.png
Symbolic link
1
linux/packaging/icons/hicolor/16x16/apps/mangayomi.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
|
||||
1
linux/packaging/icons/hicolor/256x256/apps/mangayomi.png
Symbolic link
1
linux/packaging/icons/hicolor/256x256/apps/mangayomi.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
|
||||
1
linux/packaging/icons/hicolor/32x32/apps/mangayomi.png
Symbolic link
1
linux/packaging/icons/hicolor/32x32/apps/mangayomi.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
|
||||
1
linux/packaging/icons/hicolor/512x512/apps/mangayomi.png
Symbolic link
1
linux/packaging/icons/hicolor/512x512/apps/mangayomi.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
|
||||
1
linux/packaging/icons/hicolor/64x64/apps/mangayomi.png
Symbolic link
1
linux/packaging/icons/hicolor/64x64/apps/mangayomi.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../../../macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
|
||||
Loading…
Reference in a new issue