Compare commits
13 Commits
89e0120ca6
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
| b31ef97204 | |||
| 596e8265f7 | |||
| eb173ac072 | |||
| 2619dc2c94 | |||
| e675f125d4 | |||
| 4f6b534093 | |||
| b3c906a16e | |||
| 950e143faf | |||
| efbce87d5c | |||
| 28e8b322d0 | |||
| 6c45a80623 | |||
| 413ee70923 | |||
| 851c9d701f |
9
go.mod
9
go.mod
@@ -12,7 +12,11 @@ require (
|
|||||||
golang.org/x/net v0.53.0
|
golang.org/x/net v0.53.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/hashicorp/golang-lru/v2 v2.0.7
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.12.0
|
||||||
|
github.com/pressly/goose/v3 v3.27.1
|
||||||
|
go.uber.org/fx v1.24.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
@@ -21,7 +25,6 @@ require (
|
|||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.12.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
@@ -35,7 +38,6 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pressly/goose/v3 v3.27.1 // indirect
|
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
@@ -43,7 +45,6 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go.uber.org/dig v1.19.0 // indirect
|
go.uber.org/dig v1.19.0 // indirect
|
||||||
go.uber.org/fx v1.24.0 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.26.0 // indirect
|
go.uber.org/zap v1.26.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
|||||||
35
go.sum
35
go.sum
@@ -1,7 +1,5 @@
|
|||||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
|
||||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
@@ -15,13 +13,18 @@ github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCc
|
|||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
@@ -33,17 +36,15 @@ github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
|
|||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
|
||||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
|
||||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
@@ -61,8 +62,11 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
|
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
|
||||||
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
|
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
|
||||||
@@ -72,6 +76,8 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
|
|||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
|
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/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -83,10 +89,14 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
@@ -94,6 +104,10 @@ go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
|||||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||||
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
|
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
|
||||||
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
@@ -177,4 +191,13 @@ google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBN
|
|||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0=
|
||||||
|
modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY=
|
||||||
|
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/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||||
|
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -257,7 +256,6 @@ func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Dur
|
|||||||
if !isEmptyResult(out) {
|
if !isEmptyResult(out) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("jikan: cache hit for %s but data is empty, refetching", cacheKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stale any
|
var stale any
|
||||||
@@ -273,7 +271,6 @@ func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Dur
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !errors.Is(err, context.Canceled) {
|
if !errors.Is(err, context.Canceled) {
|
||||||
log.Printf("jikan: stale cache unmarshal failed or empty, falling back to error: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -281,7 +278,6 @@ func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Dur
|
|||||||
|
|
||||||
// Don't cache empty results to avoid caching failures
|
// Don't cache empty results to avoid caching failures
|
||||||
if isEmptyResult(out) {
|
if isEmptyResult(out) {
|
||||||
log.Printf("jikan: fetched data for %s is empty, not caching", cacheKey)
|
|
||||||
return fmt.Errorf("jikan: empty response for %s", cacheKey)
|
return fmt.Errorf("jikan: empty response for %s", cacheKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ func (c *Client) GetFullRelations(ctx context.Context, id int) ([]RelationEntry,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
c.EnqueueAnimeFetchRetry(gCtx, entry.ID, err)
|
c.EnqueueAnimeFetchRetry(gCtx, entry.ID, err)
|
||||||
log.Printf("relations: skipping related anime %d for root %d: %v", entry.ID, id, err)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"mal/internal/domain"
|
"mal/internal/domain"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -23,7 +22,7 @@ func NewAnimeHandler(svc domain.AnimeService, watchlistSvc domain.WatchlistServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *AnimeHandler) Register(r *gin.Engine) {
|
func (h *AnimeHandler) Register(r *gin.Engine) {
|
||||||
log.Println("Registering anime routes")
|
|
||||||
r.GET("/", h.HandleCatalog)
|
r.GET("/", h.HandleCatalog)
|
||||||
r.GET("/api/catalog/airing", h.HandleCatalogAiring)
|
r.GET("/api/catalog/airing", h.HandleCatalogAiring)
|
||||||
r.GET("/api/catalog/popular", h.HandleCatalogPopular)
|
r.GET("/api/catalog/popular", h.HandleCatalogPopular)
|
||||||
@@ -76,7 +75,6 @@ func (h *AnimeHandler) renderCatalogSection(c *gin.Context, section string) {
|
|||||||
}
|
}
|
||||||
data, err := h.svc.GetCatalogSection(c.Request.Context(), userID, section)
|
data, err := h.svc.GetCatalogSection(c.Request.Context(), userID, section)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("catalog %s error: %v", section, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +120,6 @@ func (h *AnimeHandler) renderDiscoverSection(c *gin.Context, section string) {
|
|||||||
}
|
}
|
||||||
data, err := h.svc.GetDiscoverSection(c.Request.Context(), userID, section)
|
data, err := h.svc.GetDiscoverSection(c.Request.Context(), userID, section)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("discover %s error: %v", section, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +160,6 @@ func (h *AnimeHandler) HandleBrowse(c *gin.Context) {
|
|||||||
|
|
||||||
res, err := h.svc.SearchAdvanced(c.Request.Context(), q, animeType, status, orderBy, sort, genres, sfw, page, 24)
|
res, err := h.svc.SearchAdvanced(c.Request.Context(), q, animeType, status, orderBy, sort, genres, sfw, page, 24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("browse error: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user, _ := c.Get("User")
|
user, _ := c.Get("User")
|
||||||
@@ -253,10 +249,32 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user, _ := c.Get("User")
|
user, _ := c.Get("User")
|
||||||
|
status := ""
|
||||||
|
var watchlistIDs []int64
|
||||||
|
ep := 1
|
||||||
|
var cwSeconds float64
|
||||||
|
if u, ok := user.(*domain.User); ok {
|
||||||
|
entry, err := h.watchlistSvc.GetWatchListEntry(c.Request.Context(), u.ID, int64(id))
|
||||||
|
if err == nil {
|
||||||
|
status = entry.Status
|
||||||
|
watchlistIDs = []int64{entry.AnimeID}
|
||||||
|
}
|
||||||
|
|
||||||
|
cwEntry, err := h.watchlistSvc.GetContinueWatchingEntry(c.Request.Context(), u.ID, int64(id))
|
||||||
|
if err == nil && cwEntry.CurrentEpisode.Valid {
|
||||||
|
ep = int(cwEntry.CurrentEpisode.Int64)
|
||||||
|
cwSeconds = cwEntry.CurrentTimeSeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
|
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
|
||||||
"Anime": anime,
|
"Anime": anime,
|
||||||
"CurrentPath": fmt.Sprintf("/anime/%d", id),
|
"CurrentPath": fmt.Sprintf("/anime/%d", id),
|
||||||
"User": user,
|
"User": user,
|
||||||
|
"Status": status,
|
||||||
|
"WatchlistIDs": watchlistIDs,
|
||||||
|
"ContinueWatchingEp": ep,
|
||||||
|
"ContinueWatchingTime": cwSeconds,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ func AuthMiddleware(svc domain.AuthService) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Allow access to login, logout and static assets without authentication
|
// Allow access to login, logout and static assets without authentication
|
||||||
if c.Request.URL.Path == "/login" || c.Request.URL.Path == "/logout" ||
|
if c.Request.URL.Path == "/login" || c.Request.URL.Path == "/logout" ||
|
||||||
len(c.Request.URL.Path) >= 7 && c.Request.URL.Path[:7] == "/static" ||
|
len(c.Request.URL.Path) >= 7 && c.Request.URL.Path[:7] == "/static" ||
|
||||||
len(c.Request.URL.Path) >= 5 && c.Request.URL.Path[:5] == "/dist" {
|
len(c.Request.URL.Path) >= 5 && c.Request.URL.Path[:5] == "/dist" {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
type PlaybackService interface {
|
type PlaybackService interface {
|
||||||
BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (map[string]any, error)
|
BuildWatchData(ctx context.Context, animeID int, titleCandidates []string, episode string, mode string, userID string) (map[string]any, error)
|
||||||
SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error
|
SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error
|
||||||
|
CompleteAnime(ctx context.Context, userID string, animeID int64) error
|
||||||
ResolveProxyToken(token string) (string, string, error)
|
ResolveProxyToken(token string) (string, string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,5 +35,7 @@ type PlaybackRepository interface {
|
|||||||
GetWatchListEntry(ctx context.Context, params db.GetWatchListEntryParams) (db.WatchListEntry, error)
|
GetWatchListEntry(ctx context.Context, params db.GetWatchListEntryParams) (db.WatchListEntry, error)
|
||||||
GetContinueWatchingEntry(ctx context.Context, params db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
|
GetContinueWatchingEntry(ctx context.Context, params db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
|
||||||
SaveWatchProgress(ctx context.Context, params db.SaveWatchProgressParams) error
|
SaveWatchProgress(ctx context.Context, params db.SaveWatchProgressParams) error
|
||||||
|
UpsertWatchListEntry(ctx context.Context, params db.UpsertWatchListEntryParams) (db.WatchListEntry, error)
|
||||||
UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
|
UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
|
||||||
|
DeleteContinueWatchingEntry(ctx context.Context, params db.DeleteContinueWatchingEntryParams) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type WatchlistService interface {
|
|||||||
UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error
|
UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error
|
||||||
RemoveEntry(ctx context.Context, userID string, animeID int64) error
|
RemoveEntry(ctx context.Context, userID string, animeID int64) error
|
||||||
GetWatchlist(ctx context.Context, userID string) ([]UserWatchListRow, error)
|
GetWatchlist(ctx context.Context, userID string) ([]UserWatchListRow, error)
|
||||||
|
GetWatchListEntry(ctx context.Context, userID string, animeID int64) (WatchlistEntry, error)
|
||||||
GetContinueWatchingEntry(ctx context.Context, userID string, animeID int64) (db.ContinueWatchingEntry, error)
|
GetContinueWatchingEntry(ctx context.Context, userID string, animeID int64) (db.ContinueWatchingEntry, error)
|
||||||
DeleteContinueWatching(ctx context.Context, userID string, animeID int64) error
|
DeleteContinueWatching(ctx context.Context, userID string, animeID int64) error
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,7 @@ type WatchlistRepository interface {
|
|||||||
UpsertWatchListEntry(ctx context.Context, arg db.UpsertWatchListEntryParams) (db.WatchListEntry, error)
|
UpsertWatchListEntry(ctx context.Context, arg db.UpsertWatchListEntryParams) (db.WatchListEntry, error)
|
||||||
DeleteWatchListEntry(ctx context.Context, arg db.DeleteWatchListEntryParams) error
|
DeleteWatchListEntry(ctx context.Context, arg db.DeleteWatchListEntryParams) error
|
||||||
GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error)
|
GetUserWatchList(ctx context.Context, userID string) ([]db.GetUserWatchListRow, error)
|
||||||
|
GetWatchListEntry(ctx context.Context, arg db.GetWatchListEntryParams) (db.WatchListEntry, error)
|
||||||
GetContinueWatchingEntry(ctx context.Context, arg db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
|
GetContinueWatchingEntry(ctx context.Context, arg db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error)
|
||||||
DeleteContinueWatchingEntry(ctx context.Context, arg db.DeleteContinueWatchingEntryParams) error
|
DeleteContinueWatchingEntry(ctx context.Context, arg db.DeleteContinueWatchingEntryParams) error
|
||||||
SaveWatchProgress(ctx context.Context, arg db.SaveWatchProgressParams) error
|
SaveWatchProgress(ctx context.Context, arg db.SaveWatchProgressParams) error
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"mal/internal/domain"
|
"mal/internal/domain"
|
||||||
"mal/pkg/net/proxytransport"
|
"mal/pkg/net/proxytransport"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -33,16 +32,16 @@ func NewPlaybackHandler(svc domain.PlaybackService, animeSvc domain.AnimeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *PlaybackHandler) Register(r *gin.Engine) {
|
func (h *PlaybackHandler) Register(r *gin.Engine) {
|
||||||
log.Println("Registering playback routes")
|
|
||||||
r.GET("/anime/:id/watch", h.HandleWatchPage)
|
r.GET("/anime/:id/watch", h.HandleWatchPage)
|
||||||
r.POST("/api/watch-progress", h.HandleSaveProgress)
|
r.POST("/api/watch-progress", h.HandleSaveProgress)
|
||||||
|
r.POST("/api/watch-complete", h.HandleWatchComplete)
|
||||||
r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails)
|
r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails)
|
||||||
r.GET("/watch/proxy/stream", h.HandleProxyStream)
|
r.GET("/watch/proxy/stream", h.HandleProxyStream)
|
||||||
r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle)
|
r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
|
func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
|
||||||
log.Printf("Route /anime/:id/watch triggered for ID: %s", c.Param("id"))
|
|
||||||
id, _ := strconv.Atoi(c.Param("id"))
|
id, _ := strconv.Atoi(c.Param("id"))
|
||||||
ep := c.DefaultQuery("ep", "1")
|
ep := c.DefaultQuery("ep", "1")
|
||||||
mode := c.DefaultQuery("mode", "sub")
|
mode := c.DefaultQuery("mode", "sub")
|
||||||
@@ -55,8 +54,6 @@ func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
|
|||||||
|
|
||||||
data, err := h.svc.BuildWatchData(c.Request.Context(), id, []string{}, ep, mode, userID)
|
data, err := h.svc.BuildWatchData(c.Request.Context(), id, []string{}, ep, mode, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("BuildWatchData failed for ID %d: %v", id, err)
|
|
||||||
// Try to at least get anime info for the error page
|
|
||||||
anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id)
|
anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id)
|
||||||
c.HTML(http.StatusOK, "watch.gohtml", gin.H{
|
c.HTML(http.StatusOK, "watch.gohtml", gin.H{
|
||||||
"Error": err.Error(),
|
"Error": err.Error(),
|
||||||
@@ -69,7 +66,6 @@ func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("BuildWatchData succeeded for ID %d", id)
|
|
||||||
|
|
||||||
// Merge data from service with handler-specific context
|
// Merge data from service with handler-specific context
|
||||||
responseData := gin.H{
|
responseData := gin.H{
|
||||||
@@ -81,7 +77,6 @@ func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "watch.gohtml", responseData)
|
c.HTML(http.StatusOK, "watch.gohtml", responseData)
|
||||||
log.Printf("c.HTML finished for ID %d", id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
|
func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
|
||||||
@@ -111,6 +106,32 @@ func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) {
|
|||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *PlaybackHandler) HandleWatchComplete(c *gin.Context) {
|
||||||
|
user, _ := c.Get("User")
|
||||||
|
userID := ""
|
||||||
|
if u, ok := user.(*domain.User); ok {
|
||||||
|
userID = u.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
MalID int64 `json:"mal_id"`
|
||||||
|
Episode int `json:"episode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.Status(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.svc.CompleteAnime(c.Request.Context(), userID, req.MalID)
|
||||||
|
if err != nil {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) {
|
func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("animeId"))
|
id, err := strconv.Atoi(c.Param("animeId"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,7 +141,6 @@ func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) {
|
|||||||
|
|
||||||
allEpisodes, err := h.animeSvc.GetAllEpisodes(c.Request.Context(), id)
|
allEpisodes, err := h.animeSvc.GetAllEpisodes(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to fetch thumbnails/episodes: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id)
|
anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id)
|
||||||
@@ -168,7 +188,6 @@ func (h *PlaybackHandler) HandleProxyStream(c *gin.Context) {
|
|||||||
|
|
||||||
targetURL, referer, err := h.svc.ResolveProxyToken(token)
|
targetURL, referer, err := h.svc.ResolveProxyToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("proxy token error: %v", err)
|
|
||||||
c.Status(http.StatusForbidden)
|
c.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -185,7 +204,6 @@ func (h *PlaybackHandler) HandleProxyStream(c *gin.Context) {
|
|||||||
|
|
||||||
resp, err := h.streamingClient.Do(req)
|
resp, err := h.streamingClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("proxy stream fetch error: %v", err)
|
|
||||||
c.Status(http.StatusBadGateway)
|
c.Status(http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -212,7 +230,6 @@ func (h *PlaybackHandler) HandleProxySubtitle(c *gin.Context) {
|
|||||||
|
|
||||||
targetURL, referer, err := h.svc.ResolveProxyToken(token)
|
targetURL, referer, err := h.svc.ResolveProxyToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("proxy subtitle token error: %v", err)
|
|
||||||
c.Status(http.StatusForbidden)
|
c.Status(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -235,7 +252,6 @@ func (h *PlaybackHandler) HandleProxySubtitle(c *gin.Context) {
|
|||||||
|
|
||||||
resp, err := h.proxyClient.Do(req)
|
resp, err := h.proxyClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("proxy subtitle fetch error: %v", err)
|
|
||||||
c.Status(http.StatusBadGateway)
|
c.Status(http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -243,7 +259,6 @@ func (h *PlaybackHandler) HandleProxySubtitle(c *gin.Context) {
|
|||||||
|
|
||||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024))
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("proxy subtitle read error: %v", err)
|
|
||||||
c.Status(http.StatusBadGateway)
|
c.Status(http.StatusBadGateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ func (r *playbackRepository) SaveWatchProgress(ctx context.Context, params db.Sa
|
|||||||
return r.queries.SaveWatchProgress(ctx, params)
|
return r.queries.SaveWatchProgress(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *playbackRepository) UpsertWatchListEntry(ctx context.Context, params db.UpsertWatchListEntryParams) (db.WatchListEntry, error) {
|
||||||
|
return r.queries.UpsertWatchListEntry(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *playbackRepository) UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) {
|
func (r *playbackRepository) UpsertContinueWatchingEntry(ctx context.Context, params db.UpsertContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) {
|
||||||
return r.queries.UpsertContinueWatchingEntry(ctx, params)
|
return r.queries.UpsertContinueWatchingEntry(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *playbackRepository) DeleteContinueWatchingEntry(ctx context.Context, params db.DeleteContinueWatchingEntryParams) error {
|
||||||
|
return r.queries.DeleteContinueWatchingEntry(ctx, params)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"mal/integrations/jikan"
|
"mal/integrations/jikan"
|
||||||
"mal/internal/db"
|
"mal/internal/db"
|
||||||
"mal/internal/domain"
|
"mal/internal/domain"
|
||||||
@@ -188,6 +187,7 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
|
|||||||
// 3. Get start time from progress
|
// 3. Get start time from progress
|
||||||
startTime := 0.0
|
startTime := 0.0
|
||||||
var watchlistStatus string
|
var watchlistStatus string
|
||||||
|
var watchlistIDs []int64
|
||||||
if userID != "" {
|
if userID != "" {
|
||||||
entry, err := s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
|
entry, err := s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@@ -195,6 +195,7 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
|
|||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
watchlistStatus = entry.Status
|
watchlistStatus = entry.Status
|
||||||
|
watchlistIDs = []int64{entry.AnimeID}
|
||||||
if entry.CurrentEpisode.Valid && strconv.FormatInt(entry.CurrentEpisode.Int64, 10) == episode {
|
if entry.CurrentEpisode.Valid && strconv.FormatInt(entry.CurrentEpisode.Int64, 10) == episode {
|
||||||
startTime = entry.CurrentTimeSeconds
|
startTime = entry.CurrentTimeSeconds
|
||||||
}
|
}
|
||||||
@@ -215,7 +216,6 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
|
|||||||
// 4. Get Episodes list
|
// 4. Get Episodes list
|
||||||
jikanEpisodes, err := s.jikan.GetAllEpisodes(ctx, animeID)
|
jikanEpisodes, err := s.jikan.GetAllEpisodes(ctx, animeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to fetch episodes from jikan: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback/Fill episodes if needed
|
// Fallback/Fill episodes if needed
|
||||||
@@ -318,10 +318,42 @@ func (s *playbackService) BuildWatchData(ctx context.Context, animeID int, title
|
|||||||
"Episodes": domainEpisodes,
|
"Episodes": domainEpisodes,
|
||||||
"CurrentEpID": episode,
|
"CurrentEpID": episode,
|
||||||
"WatchlistStatus": watchlistStatus,
|
"WatchlistStatus": watchlistStatus,
|
||||||
|
"WatchlistIDs": watchlistIDs,
|
||||||
"Seasons": seasons,
|
"Seasons": seasons,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *playbackService) CompleteAnime(ctx context.Context, userID string, animeID int64) error {
|
||||||
|
entry, err := s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
|
||||||
|
UserID: userID,
|
||||||
|
AnimeID: animeID,
|
||||||
|
})
|
||||||
|
if err != nil || entry.Status != "completed" {
|
||||||
|
_, err = s.repo.UpsertWatchListEntry(ctx, db.UpsertWatchListEntryParams{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
UserID: userID,
|
||||||
|
AnimeID: animeID,
|
||||||
|
Status: "completed",
|
||||||
|
CurrentEpisode: sql.NullInt64{Valid: false},
|
||||||
|
CurrentTimeSeconds: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = s.repo.DeleteContinueWatchingEntry(ctx, db.DeleteContinueWatchingEntryParams{
|
||||||
|
UserID: userID,
|
||||||
|
AnimeID: animeID,
|
||||||
|
})
|
||||||
|
return s.repo.SaveWatchProgress(ctx, db.SaveWatchProgressParams{
|
||||||
|
UserID: userID,
|
||||||
|
AnimeID: animeID,
|
||||||
|
CurrentEpisode: sql.NullInt64{Valid: false},
|
||||||
|
CurrentTimeSeconds: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *playbackService) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error {
|
func (s *playbackService) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64) error {
|
||||||
_, err := s.repo.UpsertContinueWatchingEntry(ctx, db.UpsertContinueWatchingEntryParams{
|
_, err := s.repo.UpsertContinueWatchingEntry(ctx, db.UpsertContinueWatchingEntryParams{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ func (r *watchlistRepository) GetUserWatchList(ctx context.Context, userID strin
|
|||||||
return r.queries.GetUserWatchList(ctx, userID)
|
return r.queries.GetUserWatchList(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *watchlistRepository) GetWatchListEntry(ctx context.Context, arg db.GetWatchListEntryParams) (db.WatchListEntry, error) {
|
||||||
|
return r.queries.GetWatchListEntry(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *watchlistRepository) GetContinueWatchingEntry(ctx context.Context, arg db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) {
|
func (r *watchlistRepository) GetContinueWatchingEntry(ctx context.Context, arg db.GetContinueWatchingEntryParams) (db.ContinueWatchingEntry, error) {
|
||||||
return r.queries.GetContinueWatchingEntry(ctx, arg)
|
return r.queries.GetContinueWatchingEntry(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ func (s *watchlistService) GetWatchlist(ctx context.Context, userID string) ([]d
|
|||||||
return s.repo.GetUserWatchList(ctx, userID)
|
return s.repo.GetUserWatchList(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *watchlistService) GetWatchListEntry(ctx context.Context, userID string, animeID int64) (db.WatchListEntry, error) {
|
||||||
|
return s.repo.GetWatchListEntry(ctx, db.GetWatchListEntryParams{
|
||||||
|
UserID: userID,
|
||||||
|
AnimeID: animeID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *watchlistService) GetContinueWatchingEntry(ctx context.Context, userID string, animeID int64) (db.ContinueWatchingEntry, error) {
|
func (s *watchlistService) GetContinueWatchingEntry(ctx context.Context, userID string, animeID int64) (db.ContinueWatchingEntry, error) {
|
||||||
return s.repo.GetContinueWatchingEntry(ctx, db.GetContinueWatchingEntryParams{
|
return s.repo.GetContinueWatchingEntry(ctx, db.GetContinueWatchingEntryParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const setupControls = (): void => {
|
|||||||
// mouse move in container shows controls
|
// mouse move in container shows controls
|
||||||
state.container.addEventListener('mousemove', showControls);
|
state.container.addEventListener('mousemove', showControls);
|
||||||
|
|
||||||
// initial sync
|
// initial sync — check actual video state since inline script may have started playback
|
||||||
updatePlayPauseIcons(false);
|
updatePlayPauseIcons(!state.video.paused);
|
||||||
syncVolumeUI();
|
syncVolumeUI();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import DOMPurify from 'dompurify';
|
|
||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks anime as completed when final episode finishes.
|
|
||||||
* Calls completion API, updates dropdown UI, adds to watchlist.
|
|
||||||
* Retries up to 2 times on failure.
|
|
||||||
*/
|
|
||||||
export const completeAnime = async (episodeNumber: number): Promise<void> => {
|
export const completeAnime = async (episodeNumber: number): Promise<void> => {
|
||||||
if (state.completionSent || !state.malID || !episodeNumber) return;
|
if (state.completionSent || !state.malID || !episodeNumber) return;
|
||||||
state.completionSent = true;
|
state.completionSent = true;
|
||||||
@@ -20,7 +14,6 @@ export const completeAnime = async (episodeNumber: number): Promise<void> => {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
state.completionSent = false;
|
state.completionSent = false;
|
||||||
// retry
|
|
||||||
if (state.completionAttempts < 2) {
|
if (state.completionAttempts < 2) {
|
||||||
state.completionAttempts++;
|
state.completionAttempts++;
|
||||||
setTimeout(() => completeAnime(episodeNumber), 1000);
|
setTimeout(() => completeAnime(episodeNumber), 1000);
|
||||||
@@ -28,7 +21,6 @@ export const completeAnime = async (episodeNumber: number): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update dropdown trigger text
|
|
||||||
const trigger = document.querySelector('[data-dropdown-trigger]') as HTMLButtonElement | null;
|
const trigger = document.querySelector('[data-dropdown-trigger]') as HTMLButtonElement | null;
|
||||||
if (trigger) {
|
if (trigger) {
|
||||||
trigger.textContent = 'Completed ';
|
trigger.textContent = 'Completed ';
|
||||||
@@ -37,37 +29,6 @@ export const completeAnime = async (episodeNumber: number): Promise<void> => {
|
|||||||
caret.textContent = '▾';
|
caret.textContent = '▾';
|
||||||
trigger.appendChild(caret);
|
trigger.appendChild(caret);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add to watchlist with 'completed' status
|
|
||||||
const dropdown = document.getElementById('watch-status-dropdown');
|
|
||||||
if (dropdown) {
|
|
||||||
const payload = {
|
|
||||||
anime_id: String(state.malID),
|
|
||||||
anime_title: state.container.dataset.animeTitle ?? '',
|
|
||||||
anime_title_english: state.container.dataset.animeTitleEnglish ?? '',
|
|
||||||
anime_title_japanese: state.container.dataset.animeTitleJapanese ?? '',
|
|
||||||
anime_image: state.container.dataset.animeImage ?? '',
|
|
||||||
status: 'completed',
|
|
||||||
airing: state.container.dataset.animeAiring === 'true',
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/api/watchlist', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'HX-Request': 'true' },
|
|
||||||
body: `anime_id=${encodeURIComponent(payload.anime_id)}&anime_title=${encodeURIComponent(payload.anime_title)}&anime_title_english=${encodeURIComponent(payload.anime_title_english)}&anime_title_japanese=${encodeURIComponent(payload.anime_title_japanese)}&anime_image=${encodeURIComponent(payload.anime_image)}&status=${encodeURIComponent(payload.status)}&airing=${encodeURIComponent(String(payload.airing))}`,
|
|
||||||
credentials: 'same-origin',
|
|
||||||
})
|
|
||||||
.then(async res => {
|
|
||||||
if (!res.ok) return;
|
|
||||||
// replace dropdown with HTMX response
|
|
||||||
const html = await res.text();
|
|
||||||
const wrapper = document.createElement('span');
|
|
||||||
wrapper.id = 'watch-status-dropdown';
|
|
||||||
wrapper.innerHTML = DOMPurify.sanitize(html);
|
|
||||||
dropdown.replaceWith(wrapper);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
state.completionSent = false;
|
state.completionSent = false;
|
||||||
if (state.completionAttempts < 2) {
|
if (state.completionAttempts < 2) {
|
||||||
|
|||||||
@@ -66,9 +66,10 @@ const initPlayer = (): void => {
|
|||||||
const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement | null;
|
const progressWrap = container.querySelector('[data-progress-wrap]') as HTMLElement | null;
|
||||||
|
|
||||||
// build video src from mode, token, and saved quality preference
|
// build video src from mode, token, and saved quality preference
|
||||||
|
// Only set if not already provided by the inline script during HTML parsing
|
||||||
const preferredQuality = localStorage.getItem('mal:preferred-quality') || 'best';
|
const preferredQuality = localStorage.getItem('mal:preferred-quality') || 'best';
|
||||||
const streamToken = state.modeSources[state.currentMode]?.token;
|
const streamToken = state.modeSources[state.currentMode]?.token;
|
||||||
if (streamToken) {
|
if (!state.video.src && streamToken) {
|
||||||
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(state.currentMode)}&token=${encodeURIComponent(streamToken)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(state.currentMode)}&token=${encodeURIComponent(streamToken)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ const initPlayer = (): void => {
|
|||||||
updateAutoSkipButton();
|
updateAutoSkipButton();
|
||||||
showControls();
|
showControls();
|
||||||
|
|
||||||
state.video.addEventListener('loadedmetadata', () => {
|
const onLoadedMetadata = (): void => {
|
||||||
loading && (loading.style.display = 'none');
|
loading && (loading.style.display = 'none');
|
||||||
invalidateBounds();
|
invalidateBounds();
|
||||||
|
|
||||||
@@ -104,11 +105,19 @@ const initPlayer = (): void => {
|
|||||||
state.video.currentTime = state.pendingSeekTime;
|
state.video.currentTime = state.pendingSeekTime;
|
||||||
state.pendingSeekTime = null;
|
state.pendingSeekTime = null;
|
||||||
}
|
}
|
||||||
if (state.shouldAutoPlay) state.video.play().catch(() => {});
|
// autoplay if not already playing (inline script may have already called play())
|
||||||
|
if (state.shouldAutoPlay || state.video.paused) state.video.play().catch(() => {});
|
||||||
|
|
||||||
updateTimeline(state.video.currentTime);
|
updateTimeline(state.video.currentTime);
|
||||||
updateSkipButton(state.video.currentTime);
|
updateSkipButton(state.video.currentTime);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
state.video.addEventListener('loadedmetadata', onLoadedMetadata);
|
||||||
|
// inline script runs during HTML parsing before initPlayer; if metadata
|
||||||
|
// already loaded, fire the handler immediately
|
||||||
|
if (state.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
||||||
|
onLoadedMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
state.video.addEventListener('waiting', () => {
|
state.video.addEventListener('waiting', () => {
|
||||||
loading && (loading.style.display = 'flex');
|
loading && (loading.style.display = 'flex');
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
{{if $anime.ShortRating}}<span class="flex items-center gap-1.5"><span>•</span>{{$anime.ShortRating}}</span>{{end}}
|
{{if $anime.ShortRating}}<span class="flex items-center gap-1.5"><span>•</span>{{$anime.ShortRating}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "watchlist_actions" dict "Anime" $anime "User" .User "Status" .Status}}
|
{{template "watchlist_actions" dict "Anime" $anime "User" .User "Status" .Status "ContinueWatchingEp" .ContinueWatchingEp "ContinueWatchingTime" .ContinueWatchingTime}}
|
||||||
|
|
||||||
<div class="flex flex-col gap-12 lg:flex-row">
|
<div class="flex flex-col gap-12 lg:flex-row">
|
||||||
<div class="grow lg:max-w-4xl">
|
<div class="grow lg:max-w-4xl">
|
||||||
|
|||||||
@@ -109,7 +109,20 @@
|
|||||||
const watchlistIds = new Set()
|
const watchlistIds = new Set()
|
||||||
|
|
||||||
function initWatchlist(ids) {
|
function initWatchlist(ids) {
|
||||||
ids.forEach(id => watchlistIds.add(id))
|
ids.forEach(id => watchlistIds.add(id));
|
||||||
|
const sync = () => ids.forEach(id => syncRemoveButtonVisibility(id));
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', sync);
|
||||||
|
} else {
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRemoveButtonVisibility(id) {
|
||||||
|
const container = document.getElementById('remove-watchlist-container-' + id);
|
||||||
|
if (container) {
|
||||||
|
container.classList.toggle('hidden', !watchlistIds.has(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWatchlist(id, btn) {
|
function toggleWatchlist(id, btn) {
|
||||||
@@ -169,10 +182,7 @@ if (window.showToast) showToast({ message: 'Something went wrong' })
|
|||||||
const statusDisplay = document.getElementById('watchlist-status-display-' + id)
|
const statusDisplay = document.getElementById('watchlist-status-display-' + id)
|
||||||
if (statusDisplay) {
|
if (statusDisplay) {
|
||||||
statusDisplay.textContent = inWatchlist ? 'Plan to Watch' : 'Add to Watchlist'
|
statusDisplay.textContent = inWatchlist ? 'Plan to Watch' : 'Add to Watchlist'
|
||||||
const removeContainer = document.getElementById('remove-watchlist-container-' + id)
|
syncRemoveButtonVisibility(id)
|
||||||
if (removeContainer) {
|
|
||||||
removeContainer.classList.toggle('hidden', !inWatchlist)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +196,57 @@ if (window.showToast) showToast({ message: 'Something went wrong' })
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateWatchlist(id, status, display, btn) {
|
||||||
|
fetch('/api/watchlist', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ animeId: id, status: status })
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
watchlistIds.add(id);
|
||||||
|
document.getElementById('watchlist-status-display-' + id).textContent = display;
|
||||||
|
syncRemoveButtonVisibility(id);
|
||||||
|
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
|
||||||
|
const button = icon.closest('button');
|
||||||
|
if (button) {
|
||||||
|
const malId = button.dataset.malId;
|
||||||
|
if (malId && parseInt(malId) === id) {
|
||||||
|
button.classList.add('in-watchlist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const dropdown = btn.closest('ui-dropdown');
|
||||||
|
if (dropdown) dropdown.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWatchlist(id, btn) {
|
||||||
|
fetch('/api/watchlist/' + id, { method: 'DELETE' }).then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
watchlistIds.delete(id);
|
||||||
|
document.getElementById('watchlist-status-display-' + id).textContent = 'Add to Watchlist';
|
||||||
|
syncRemoveButtonVisibility(id);
|
||||||
|
if (window.showToast) showToast({ message: 'Removed from watchlist' });
|
||||||
|
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
|
||||||
|
const button = icon.closest('button');
|
||||||
|
if (button) {
|
||||||
|
const malId = button.dataset.malId;
|
||||||
|
if (malId && parseInt(malId) === id) {
|
||||||
|
button.classList.remove('in-watchlist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (btn) {
|
||||||
|
const dropdown = btn.closest('ui-dropdown');
|
||||||
|
if (dropdown) dropdown.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-background text-foreground">
|
<body class="bg-background text-foreground">
|
||||||
|
|||||||
@@ -20,19 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div id="anime-grid" class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
|
<div id="anime-grid" class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
|
||||||
{{range $i, $anime := .Animes}}
|
{{range .Animes}}
|
||||||
{{$isThreshold := eq (add $i 1) (sub (len $.Animes) 8)}}
|
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||||
{{if and $isThreshold $.HasNextPage}}
|
{{end}}
|
||||||
<div hx-get="/browse?q={{$.Query}}&type={{$.Type}}&status={{$.Status}}&order_by={{$.OrderBy}}&sort={{$.Sort}}&sfw={{$.SFW}}&{{genresParams $.Genres}}&page={{$.NextPage}}"
|
{{if .HasNextPage}}
|
||||||
hx-trigger="revealed"
|
{{template "browse_sentinel" .}}
|
||||||
hx-swap="afterend"
|
|
||||||
hx-target="this"
|
|
||||||
class="contents">
|
|
||||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -41,19 +33,18 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "anime_card_scroll"}}
|
{{define "anime_card_scroll"}}
|
||||||
{{$count := len .Animes}}
|
{{range .Animes}}
|
||||||
{{range $i, $anime := .Animes}}
|
{{template "anime_card" dict "Anime" . "WithActions" true "IsWatchlist" (index $.WatchlistMap .MalID)}}
|
||||||
{{$isThreshold := eq (add $i 1) (sub $count 8)}}
|
{{end}}
|
||||||
{{if and $isThreshold $.HasNextPage}}
|
{{if .HasNextPage}}
|
||||||
<div hx-get="/browse?q={{$.Query}}&type={{$.Type}}&status={{$.Status}}&order_by={{$.OrderBy}}&sort={{$.Sort}}&sfw={{$.SFW}}&{{genresParams $.Genres}}&page={{$.NextPage}}"
|
{{template "browse_sentinel" .}}
|
||||||
hx-trigger="revealed"
|
|
||||||
hx-swap="afterend"
|
|
||||||
hx-target="this"
|
|
||||||
class="contents">
|
|
||||||
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
{{template "anime_card" dict "Anime" $anime "WithActions" true "IsWatchlist" (index $.WatchlistMap $anime.MalID)}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "browse_sentinel"}}
|
||||||
|
<div hx-get="/browse?q={{.Query}}&type={{.Type}}&status={{.Status}}&order_by={{.OrderBy}}&sort={{.Sort}}&sfw={{.SFW}}&{{genresParams .Genres}}&page={{.NextPage}}"
|
||||||
|
hx-trigger="intersect once"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target="this"
|
||||||
|
class="col-span-full h-px"></div>
|
||||||
|
{{end}}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div id="continue-watching-{{.AnimeID}}" class="continue-watching-item group relative w-70 shrink-0 snap-start space-y-2 2xl:w-lg">
|
<div id="continue-watching-{{.AnimeID}}" class="continue-watching-item group relative w-70 shrink-0 snap-start space-y-2 2xl:w-lg">
|
||||||
<div class="bg-background/80 relative aspect-video w-full overflow-hidden">
|
<div class="bg-background/80 relative aspect-video w-full overflow-hidden">
|
||||||
<a href="/anime/{{.AnimeID}}/watch" class="block h-full w-full">
|
<a href="/anime/{{.AnimeID}}/watch{{if .CurrentEpisode.Valid}}?ep={{.CurrentEpisode.Int64}}{{end}}" class="block h-full w-full">
|
||||||
<img src="{{if .ImageUrl}}{{.ImageUrl}}{{else}}https://placehold.co/500x500{{end}}" alt="{{$title}}" class="h-full w-full object-cover" />
|
<img src="{{if .ImageUrl}}{{.ImageUrl}}{{else}}https://placehold.co/500x500{{end}}" alt="{{$title}}" class="h-full w-full object-cover" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a href="/anime/{{.AnimeID}}/watch" class="block">
|
<a href="/anime/{{.AnimeID}}/watch{{if .CurrentEpisode.Valid}}?ep={{.CurrentEpisode.Int64}}{{end}}" class="block">
|
||||||
<h3 class="text-foreground truncate text-lg font-normal">
|
<h3 class="text-foreground truncate text-lg font-normal">
|
||||||
{{$title}}
|
{{$title}}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -14,7 +14,27 @@
|
|||||||
class="group relative aspect-video w-full overflow-hidden bg-black">
|
class="group relative aspect-video w-full overflow-hidden bg-black">
|
||||||
|
|
||||||
|
|
||||||
<video class="h-full w-full cursor-pointer" preload="metadata" playsinline autoplay></video>
|
<video class="h-full w-full cursor-pointer" preload="metadata" playsinline></video>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var p = document.currentScript.closest('[data-video-player]');
|
||||||
|
var v = p.querySelector('video');
|
||||||
|
var sources = JSON.parse(p.getAttribute('data-mode-sources') || '{}');
|
||||||
|
var mode = p.getAttribute('data-initial-mode') || 'dub';
|
||||||
|
var stored = localStorage.getItem('player-audio-mode');
|
||||||
|
if (stored && sources[stored] && sources[stored].token) mode = stored;
|
||||||
|
var src = sources[mode];
|
||||||
|
if (!src || !src.token) {
|
||||||
|
for (var k in sources) {
|
||||||
|
if (sources[k] && sources[k].token) { src = sources[k]; mode = k; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (src && src.token) {
|
||||||
|
v.src = '/watch/proxy/stream?mode=' + encodeURIComponent(mode) + '&token=' + encodeURIComponent(src.token);
|
||||||
|
v.play().catch(function() {});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<div data-loading class="absolute inset-0 flex items-center justify-center bg-black/60 hidden z-50">
|
<div data-loading class="absolute inset-0 flex items-center justify-center bg-black/60 hidden z-50">
|
||||||
<div class="border-accent size-10 animate-spin rounded-full border-4 border-t-transparent"></div>
|
<div class="border-accent size-10 animate-spin rounded-full border-4 border-t-transparent"></div>
|
||||||
|
|||||||
@@ -36,80 +36,28 @@
|
|||||||
<span class="font-medium text-sm text-foreground">Dropped</span>
|
<span class="font-medium text-sm text-foreground">Dropped</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div id="remove-watchlist-container-{{$anime.MalID}}" class="{{if not $status}}hidden{{end}}">
|
{{template "watchlist_remove_button" dict
|
||||||
<div class="my-1 h-px bg-border"></div>
|
"ID" $anime.MalID
|
||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-red-500/10 focus:bg-red-500/10" onclick="removeWatchlist({{$anime.MalID}})">
|
"ContainerClass" "hidden"
|
||||||
<span class="font-medium text-sm text-red-500 text-left whitespace-nowrap">Remove from Watchlist</span>
|
"DividerClass" "my-1 h-px bg-border"
|
||||||
</button>
|
"ButtonClass" "flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-red-500/10 focus:bg-red-500/10"
|
||||||
</div>
|
"SpanClass" "font-medium text-sm text-red-500 text-left whitespace-nowrap"
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ui-dropdown>
|
</ui-dropdown>
|
||||||
|
|
||||||
<a href="/anime/{{$anime.MalID}}/watch" class="bg-background-button hover:bg-background-button-hover px-5 py-2.5 text-sm font-medium text-foreground transition-colors">
|
<a href="/anime/{{$anime.MalID}}/watch{{if and .ContinueWatchingEp (ne .ContinueWatchingEp 1)}}?ep={{.ContinueWatchingEp}}{{end}}" class="bg-background-button hover:bg-background-button-hover px-5 py-2.5 text-sm font-medium text-foreground transition-colors">
|
||||||
<i class="fa-solid fa-play mr-2"></i>
|
{{if and .ContinueWatchingEp (ne .ContinueWatchingEp 1)}}Continue Episode {{.ContinueWatchingEp}}{{else}}Watch Now{{end}}
|
||||||
Watch Now
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
<script>
|
|
||||||
function updateWatchlist(id, status, display, btn) {
|
{{define "watchlist_remove_button"}}
|
||||||
fetch('/api/watchlist', {
|
<div id="remove-watchlist-container-{{.ID}}" class="{{.ContainerClass}}">
|
||||||
method: 'POST',
|
<div class="{{.DividerClass}}"></div>
|
||||||
headers: { 'Content-Type': 'application/json' },
|
<button class="{{.ButtonClass}}" onclick="removeWatchlist({{.ID}}, this)">
|
||||||
body: JSON.stringify({ animeId: id, status: status })
|
<span class="{{.SpanClass}}">Remove from Watchlist</span>
|
||||||
}).then(res => {
|
</button>
|
||||||
if (res.ok) {
|
</div>
|
||||||
watchlistIds.add(id);
|
|
||||||
document.getElementById('watchlist-status-display-' + id).textContent = display;
|
|
||||||
document.getElementById('remove-watchlist-container-' + id).classList.remove('hidden');
|
|
||||||
|
|
||||||
// Update all watchlist icons on the page
|
|
||||||
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
|
|
||||||
const button = icon.closest('button')
|
|
||||||
if (button) {
|
|
||||||
const malId = button.dataset.malId
|
|
||||||
if (malId && parseInt(malId) === id) {
|
|
||||||
button.classList.add('in-watchlist')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdown after a small delay to let click event finish
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const dropdown = btn.closest('ui-dropdown');
|
|
||||||
if (dropdown) dropdown.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWatchlist(id) {
|
|
||||||
fetch('/api/watchlist/' + id, { method: 'DELETE' }).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
watchlistIds.delete(id);
|
|
||||||
document.getElementById('watchlist-status-display-' + id).textContent = 'Add to Watchlist';
|
|
||||||
document.getElementById('remove-watchlist-container-' + id).classList.add('hidden');
|
|
||||||
|
|
||||||
// Update all watchlist icons on the page
|
|
||||||
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
|
|
||||||
const button = icon.closest('button')
|
|
||||||
if (button) {
|
|
||||||
const malId = button.dataset.malId
|
|
||||||
if (malId && parseInt(malId) === id) {
|
|
||||||
button.classList.remove('in-watchlist')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdown
|
|
||||||
const btn = document.getElementById('watchlist-status-display-' + id);
|
|
||||||
if (btn) {
|
|
||||||
const dropdown = btn.closest('ui-dropdown');
|
|
||||||
if (dropdown) dropdown.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div data-content class="hidden absolute z-50 min-w-40 bg-background-button shadow-2xl right-0 top-full mt-2">
|
<div data-content class="hidden absolute z-50 min-w-40 bg-background-button shadow-2xl right-0 top-full mt-2">
|
||||||
{{if .WatchlistStatus}}
|
|
||||||
<div class="flex flex-col py-1">
|
<div class="flex flex-col py-1">
|
||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'watching', 'Watching', this)">
|
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'watching', 'Watching', this)">
|
||||||
<span class="text-sm text-foreground">Watching</span>
|
<span class="text-sm text-foreground">Watching</span>
|
||||||
@@ -38,27 +37,14 @@
|
|||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'dropped', 'Dropped', this)">
|
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'dropped', 'Dropped', this)">
|
||||||
<span class="text-sm text-foreground">Dropped</span>
|
<span class="text-sm text-foreground">Dropped</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="border-t border-border my-1"></div>
|
{{template "watchlist_remove_button" dict
|
||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-red-500/10" onclick="removeWatchlist({{$anime.MalID}}, this)">
|
"ID" $anime.MalID
|
||||||
<span class="text-sm text-red-400 whitespace-nowrap">Remove from Watchlist</span>
|
"ContainerClass" "hidden"
|
||||||
</button>
|
"DividerClass" "border-t border-border my-1"
|
||||||
|
"ButtonClass" "flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-red-500/10"
|
||||||
|
"SpanClass" "text-sm text-red-400 whitespace-nowrap"
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
|
||||||
<div class="flex flex-col py-1">
|
|
||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'watching', 'Watching', this)">
|
|
||||||
<span class="text-sm text-foreground">Watching</span>
|
|
||||||
</button>
|
|
||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'completed', 'Completed', this)">
|
|
||||||
<span class="text-sm text-foreground">Completed</span>
|
|
||||||
</button>
|
|
||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'plan_to_watch', 'Plan to Watch', this)">
|
|
||||||
<span class="text-sm text-foreground">Plan to Watch</span>
|
|
||||||
</button>
|
|
||||||
<button class="flex w-full items-center px-5 py-2.5 transition-colors focus:outline-none hover:bg-surface-hover" onclick="updateWatchlist({{$anime.MalID}}, 'dropped', 'Dropped', this)">
|
|
||||||
<span class="text-sm text-foreground">Dropped</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</ui-dropdown>
|
</ui-dropdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,66 +165,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<script>
|
|
||||||
function updateWatchlist(id, status, display, btn) {
|
|
||||||
fetch('/api/watchlist', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ animeId: id, status: status })
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
watchlistIds.add(id);
|
|
||||||
document.getElementById('watchlist-status-display-' + id).textContent = display;
|
|
||||||
|
|
||||||
// Update all watchlist icons on the page
|
|
||||||
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
|
|
||||||
const button = icon.closest('button')
|
|
||||||
if (button) {
|
|
||||||
const malId = button.dataset.malId
|
|
||||||
if (malId && parseInt(malId) === id) {
|
|
||||||
button.classList.add('in-watchlist')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdown after a small delay to let click event finish
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const dropdown = btn.closest('ui-dropdown');
|
|
||||||
if (dropdown) dropdown.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWatchlist(id, btn) {
|
|
||||||
fetch('/api/watchlist/' + id, { method: 'DELETE' }).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
watchlistIds.delete(id);
|
|
||||||
document.getElementById('watchlist-status-display-' + id).textContent = 'Add to Watchlist';
|
|
||||||
if (window.showToast) showToast({ message: 'Removed from watchlist' });
|
|
||||||
|
|
||||||
// Update all watchlist icons on the page
|
|
||||||
document.querySelectorAll('.watchlist-icon').forEach(function(icon) {
|
|
||||||
const button = icon.closest('button')
|
|
||||||
if (button) {
|
|
||||||
const malId = button.dataset.malId
|
|
||||||
if (malId && parseInt(malId) === id) {
|
|
||||||
button.classList.remove('in-watchlist')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdown
|
|
||||||
if (btn) {
|
|
||||||
const dropdown = btn.closest('ui-dropdown');
|
|
||||||
if (dropdown) dropdown.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user