Add Source PornPics (#8364)
* feat(pornpics): add source pornpics * feat(pornpics): add i18n support * fix(pornpics): Default category read error * fix(pornpics): Content-Type is not set correctly * fix(pornpics): properly handle searches * fix(pornpics): properly handle searches * fix(pornpics): correct next page detection logic. Add +1 to requested image count per page,Compare actual received count with pageSize to determine next page. * fix(pornpics): set base language to en * fix(pornpics): safely handle gallery info parsing * feat(pornpics): add filter * chore(pornpics): remove unused dependency * fix(pornpics): correct category urlPart values. * refactor(pornpics): simplify category browsing logic - Remove category unselected - Treat all non-search requests as category browsing * refactor(pornpics): make a singelton object and remove comanion object * refactor(pornpics): put in class to reuse preference * refactor(pornpics): optimize chapter loading with fetchChapterList - Replace chapterFromElement with fetchChapterList - Reduce unnecessary network requests * fix(pornpics): correct CategoryType initialization * refactor(pornpics): improve method naming - Rename `addQueryPageParameter` to `addQueryParameterPage` for clarity * refactor(pornpics): improve API readability with boolean parameters - Change `buildMangasPageRequest(page: Int, period: Int)` to: `buildMangasPageRequest(page: Int, popular: Boolean)` - Replace numeric period flag with semantic boolean - Simplify request building logic * refactor(pornpics): extract category search logic to dedicated method - Extract `useSearch` as standalone method - Add enhanced validation logic for category search * refactor(pornpics): replace manual parsing with HttpUrl - Replace custom URL parsing logic with HttpUrl utility * fix(pornpics): remove invalid category options * refactor(pornpics): improve JSON error handling - Throw specific exception type when JSON parsing fails
This commit is contained in:
parent
ebd527364e
commit
334dd69fab
120
src/all/pornpics/assets/i18n/categories_en.json
Normal file
120
src/all/pornpics/assets/i18n/categories_en.json
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
[
|
||||||
|
{"id": "3089","name": "AI","link": "/ai/","flag_code": ""},
|
||||||
|
{"id": "226","name": "Amateur","link": "/amateur/","flag_code": ""},
|
||||||
|
{"id": "224","name": "Anal","link": "/anal/","flag_code": ""},
|
||||||
|
{"id": "231","name": "Anal Gape","link": "/anal-gape/","flag_code": ""},
|
||||||
|
{"id": "1853","name": "Arab","link": "/arab/","flag_code": ""},
|
||||||
|
{"id": "1163","name": "Armpit","link": "/armpit/","flag_code": ""},
|
||||||
|
{"id": "223","name": "Asian","link": "/asian/","flag_code": ""},
|
||||||
|
{"id": "254","name": "Ass","link": "/ass/","flag_code": ""},
|
||||||
|
{"id": "1707","name": "Asshole","link": "/asshole/","flag_code": ""},
|
||||||
|
{"id": "1847","name": "BBC","link": "/bbc/","flag_code": ""},
|
||||||
|
{"id": "258","name": "BBW","link": "/bbw/","flag_code": ""},
|
||||||
|
{"id": "1778","name": "BDSM","link": "/bdsm/","flag_code": ""},
|
||||||
|
{"id": "266","name": "Beach","link": "/beach/","flag_code": ""},
|
||||||
|
{"id": "2749","name": "Beautiful","link": "/beautiful/","flag_code": ""},
|
||||||
|
{"id": "930","name": "Big Clit","link": "/big-clit/","flag_code": ""},
|
||||||
|
{"id": "329","name": "Big Cock","link": "/big-cock/","flag_code": ""},
|
||||||
|
{"id": "232","name": "Big Tits","link": "/big-tits/","flag_code": ""},
|
||||||
|
{"id": "63","name": "Bikini","link": "/bikini/","flag_code": ""},
|
||||||
|
{"id": "361","name": "Blonde","link": "/blonde/","flag_code": ""},
|
||||||
|
{"id": "331","name": "Blowjob","link": "/blowjob/","flag_code": ""},
|
||||||
|
{"id": "364","name": "Bondage","link": "/bondage/","flag_code": ""},
|
||||||
|
{"id": "280","name": "Brazilian","link": "/brazilian/","flag_code": "br"},
|
||||||
|
{"id": "334","name": "Brunette","link": "/brunette/","flag_code": ""},
|
||||||
|
{"id": "304","name": "Bukkake","link": "/bukkake/","flag_code": ""},
|
||||||
|
{"id": "285","name": "Cameltoe","link": "/cameltoe/","flag_code": ""},
|
||||||
|
{"id": "2106","name": "Chinese","link": "/chinese/","flag_code": "cn"},
|
||||||
|
{"id": "41","name": "Chubby","link": "/chubby/","flag_code": ""},
|
||||||
|
{"id": "295","name": "Close Up","link": "/close-up/","flag_code": ""},
|
||||||
|
{"id": "244","name": "Cosplay","link": "/cosplay/","flag_code": ""},
|
||||||
|
{"id": "288","name": "Cowgirl","link": "/cowgirl/","flag_code": ""},
|
||||||
|
{"id": "342","name": "Creampie","link": "/creampie/","flag_code": ""},
|
||||||
|
{"id": "43","name": "Cuckold","link": "/cuckold/","flag_code": ""},
|
||||||
|
{"id": "241","name": "Cum In Mouth","link": "/cum-in-mouth/","flag_code": ""},
|
||||||
|
{"id": "336","name": "Cum In Pussy","link": "/cum-in-pussy/","flag_code": ""},
|
||||||
|
{"id": "339","name": "Cumshot","link": "/cumshot/","flag_code": ""},
|
||||||
|
{"id": "1927","name": "Curvy","link": "/curvy/","flag_code": ""},
|
||||||
|
{"id": "2031","name": "Cute","link": "/cute/","flag_code": ""},
|
||||||
|
{"id": "368","name": "Deepthroat","link": "/deepthroat/","flag_code": ""},
|
||||||
|
{"id": "341","name": "Dildo","link": "/dildo/","flag_code": ""},
|
||||||
|
{"id": "129","name": "Doggy Style","link": "/doggystyle/","flag_code": ""},
|
||||||
|
{"id": "251","name": "Double Penetration","link": "/double-penetration/","flag_code": ""},
|
||||||
|
{"id": "1706","name": "Dress","link": "/dress/","flag_code": ""},
|
||||||
|
{"id": "234","name": "Ebony","link": "/ebonies/","flag_code": ""},
|
||||||
|
{"id": "359","name": "Facial","link": "/facial/","flag_code": ""},
|
||||||
|
{"id": "347","name": "Feet","link": "/feet/","flag_code": ""},
|
||||||
|
{"id": "348","name": "Femdom","link": "/femdom/","flag_code": ""},
|
||||||
|
{"id": "2017","name": "Filipina","link": "/filipina/","flag_code": "ph"},
|
||||||
|
{"id": "272","name": "Gangbang","link": "/gangbang/","flag_code": ""},
|
||||||
|
{"id": "23","name": "Gay","link": "/gay/","flag_code": "yy"},
|
||||||
|
{"id": "267","name": "Glasses","link": "/glasses/","flag_code": ""},
|
||||||
|
{"id": "228","name": "Granny","link": "/granny/","flag_code": ""},
|
||||||
|
{"id": "1711","name": "Gym","link": "/gym/","flag_code": ""},
|
||||||
|
{"id": "227","name": "Hairy","link": "/hairy/","flag_code": ""},
|
||||||
|
{"id": "353","name": "Handjob","link": "/handjob/","flag_code": ""},
|
||||||
|
{"id": "245","name": "Hardcore","link": "/hardcore/","flag_code": ""},
|
||||||
|
{"id": "3090","name": "Hentai","link": "/hentai/","flag_code": ""},
|
||||||
|
{"id": "333","name": "High Heels","link": "/high-heels/","flag_code": ""},
|
||||||
|
{"id": "314","name": "Indian","link": "/indian/","flag_code": "in"},
|
||||||
|
{"id": "355","name": "Japanese","link": "/japanese/","flag_code": "jp"},
|
||||||
|
{"id": "25","name": "Korean","link": "/korean/","flag_code": "kr"},
|
||||||
|
{"id": "3","name": "Ladyboy","link": "/ladyboy/","flag_code": "xy"},
|
||||||
|
{"id": "345","name": "Latina","link": "/latina/","flag_code": ""},
|
||||||
|
{"id": "229","name": "Lesbian","link": "/lesbian/","flag_code": "xx"},
|
||||||
|
{"id": "330","name": "Lingerie","link": "/lingerie/","flag_code": ""},
|
||||||
|
{"id": "239","name": "MILF","link": "/milf/","flag_code": ""},
|
||||||
|
{"id": "346","name": "Maid","link": "/maid/","flag_code": ""},
|
||||||
|
{"id": "277","name": "Massage","link": "/massage/","flag_code": ""},
|
||||||
|
{"id": "290","name": "Masturbation","link": "/masturbation/","flag_code": ""},
|
||||||
|
{"id": "326","name": "Mature","link": "/mature/","flag_code": ""},
|
||||||
|
{"id": "278","name": "Missionary","link": "/missionary/","flag_code": ""},
|
||||||
|
{"id": "2095","name": "Model","link": "/model/","flag_code": ""},
|
||||||
|
{"id": "201","name": "Mom","link": "/mom/","flag_code": ""},
|
||||||
|
{"id": "29","name": "Natural Tits","link": "/natural-tits/","flag_code": ""},
|
||||||
|
{"id": "2540","name": "Nun","link": "/nun/","flag_code": ""},
|
||||||
|
{"id": "306","name": "Nurse","link": "/nurse/","flag_code": ""},
|
||||||
|
{"id": "356","name": "Office","link": "/office/","flag_code": ""},
|
||||||
|
{"id": "3046","name": "Orgasm","link": "/orgasm/","flag_code": ""},
|
||||||
|
{"id": "337","name": "Outdoor","link": "/outdoor/","flag_code": ""},
|
||||||
|
{"id": "257","name": "POV","link": "/pov/","flag_code": ""},
|
||||||
|
{"id": "259","name": "Panties","link": "/panties/","flag_code": ""},
|
||||||
|
{"id": "279","name": "Pantyhose","link": "/pantyhose/","flag_code": ""},
|
||||||
|
{"id": "264","name": "Petite","link": "/petite/","flag_code": ""},
|
||||||
|
{"id": "1906","name": "Pinay","link": "/pinay/","flag_code": ""},
|
||||||
|
{"id": "283","name": "Pissing","link": "/pissing/","flag_code": ""},
|
||||||
|
{"id": "243","name": "Pornstar","link": "/pornstar/","flag_code": ""},
|
||||||
|
{"id": "260","name": "Pregnant","link": "/pregnant/","flag_code": ""},
|
||||||
|
{"id": "298","name": "Public","link": "/public/","flag_code": ""},
|
||||||
|
{"id": "238","name": "Pussy","link": "/pussy/","flag_code": ""},
|
||||||
|
{"id": "322","name": "Pussy Licking","link": "/pussy-licking/","flag_code": ""},
|
||||||
|
{"id": "246","name": "Redhead","link": "/redhead/","flag_code": ""},
|
||||||
|
{"id": "5","name": "Russian","link": "/russian/","flag_code": "ru"},
|
||||||
|
{"id": "225","name": "Saggy Tits","link": "/saggy-tits/","flag_code": ""},
|
||||||
|
{"id": "270","name": "Schoolgirl","link": "/schoolgirl/","flag_code": ""},
|
||||||
|
{"id": "325","name": "Selfie","link": "/selfie/","flag_code": ""},
|
||||||
|
{"id": "1775","name": "Sex Doll","link": "/sex-doll/","flag_code": ""},
|
||||||
|
{"id": "2565","name": "Sexy","link": "/sexy/","flag_code": ""},
|
||||||
|
{"id": "1","name": "Shemale","link": "/shemale/","flag_code": "xy"},
|
||||||
|
{"id": "292","name": "Short Hair","link": "/short-hair/","flag_code": ""},
|
||||||
|
{"id": "305","name": "Shower","link": "/shower/","flag_code": ""},
|
||||||
|
{"id": "249","name": "Skinny","link": "/skinny/","flag_code": ""},
|
||||||
|
{"id": "236","name": "Skirt","link": "/skirt/","flag_code": ""},
|
||||||
|
{"id": "1827","name": "Solo","link": "/solo/","flag_code": ""},
|
||||||
|
{"id": "1880","name": "Step Sister","link": "/step-sister/","flag_code": ""},
|
||||||
|
{"id": "1828","name": "Stepmom","link": "/stepmom/","flag_code": ""},
|
||||||
|
{"id": "247","name": "Stockings","link": "/stockings/","flag_code": ""},
|
||||||
|
{"id": "256","name": "Tall","link": "/tall/","flag_code": ""},
|
||||||
|
{"id": "311","name": "Tattoo","link": "/tattoo/","flag_code": ""},
|
||||||
|
{"id": "281","name": "Teacher","link": "/teacher/","flag_code": ""},
|
||||||
|
{"id": "237","name": "Teen","link": "/teens/","flag_code": ""},
|
||||||
|
{"id": "265","name": "Thai","link": "/thai/","flag_code": "th"},
|
||||||
|
{"id": "2032","name": "Thick","link": "/thick/","flag_code": ""},
|
||||||
|
{"id": "230","name": "Threesome","link": "/threesome/","flag_code": ""},
|
||||||
|
{"id": "934","name": "Ukrainian","link": "/ukrainian/","flag_code": "ua"},
|
||||||
|
{"id": "273","name": "Upskirt","link": "/upskirt/","flag_code": ""},
|
||||||
|
{"id": "61","name": "Vintage","link": "/vintage/","flag_code": ""},
|
||||||
|
{"id": "2544","name": "White","link": "/white/","flag_code": ""},
|
||||||
|
{"id": "2775","name": "Yoga","link": "/yoga/","flag_code": ""},
|
||||||
|
{"id": "275","name": "Yoga Pants","link": "/yoga-pants/","flag_code": ""}
|
||||||
|
]
|
120
src/all/pornpics/assets/i18n/categories_zh.json
Normal file
120
src/all/pornpics/assets/i18n/categories_zh.json
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
[
|
||||||
|
{"id": "3089","name": "人工智能","link": "/zh/ai/","flag_code": ""},
|
||||||
|
{"id": "226","name": "新手","link": "/zh/amateur/","flag_code": ""},
|
||||||
|
{"id": "224","name": "肛门","link": "/zh/anal/","flag_code": ""},
|
||||||
|
{"id": "231","name": "肛门间隙","link": "/zh/anal-gape/","flag_code": ""},
|
||||||
|
{"id": "1853","name": "阿拉伯人","link": "/zh/arab/","flag_code": ""},
|
||||||
|
{"id": "1163","name": "腋下","link": "/zh/armpit/","flag_code": ""},
|
||||||
|
{"id": "223","name": "亚洲人","link": "/zh/asian/","flag_code": ""},
|
||||||
|
{"id": "254","name": "屁股","link": "/zh/ass/","flag_code": ""},
|
||||||
|
{"id": "1707","name": "混蛋","link": "/zh/asshole/","flag_code": ""},
|
||||||
|
{"id": "1847","name": "无套","link": "/zh/bbc/","flag_code": ""},
|
||||||
|
{"id": "258","name": "性感胖女人","link": "/zh/bbw/","flag_code": ""},
|
||||||
|
{"id": "1778","name": "BDSM","link": "/zh/bdsm/","flag_code": ""},
|
||||||
|
{"id": "266","name": "海滩","link": "/zh/beach/","flag_code": ""},
|
||||||
|
{"id": "2749","name": "美丽","link": "/zh/beautiful/","flag_code": ""},
|
||||||
|
{"id": "930","name": "大阴蒂","link": "/zh/big-clit/","flag_code": ""},
|
||||||
|
{"id": "329","name": "大屌","link": "/zh/big-cock/","flag_code": ""},
|
||||||
|
{"id": "232","name": "大山雀","link": "/zh/big-tits/","flag_code": ""},
|
||||||
|
{"id": "63","name": "比基尼","link": "/zh/bikini/","flag_code": ""},
|
||||||
|
{"id": "361","name": "金发女郎","link": "/zh/blonde/","flag_code": ""},
|
||||||
|
{"id": "331","name": "口交","link": "/zh/blowjob/","flag_code": ""},
|
||||||
|
{"id": "364","name": "奴役","link": "/zh/bondage/","flag_code": ""},
|
||||||
|
{"id": "280","name": "巴西人","link": "/zh/brazilian/","flag_code": "br"},
|
||||||
|
{"id": "334","name": "深褐色头发的白人女子","link": "/zh/brunette/","flag_code": ""},
|
||||||
|
{"id": "304","name": "颜射","link": "/zh/bukkake/","flag_code": ""},
|
||||||
|
{"id": "285","name": "骆驼趾","link": "/zh/cameltoe/","flag_code": ""},
|
||||||
|
{"id": "2106","name": "中国人","link": "/zh/chinese/","flag_code": "cn"},
|
||||||
|
{"id": "41","name": "胖胖","link": "/zh/chubby/","flag_code": ""},
|
||||||
|
{"id": "295","name": "近拍","link": "/zh/close-up/","flag_code": ""},
|
||||||
|
{"id": "244","name": "角色扮演","link": "/zh/cosplay/","flag_code": ""},
|
||||||
|
{"id": "288","name": "女牛仔","link": "/zh/cowgirl/","flag_code": ""},
|
||||||
|
{"id": "342","name": "体内射精","link": "/zh/creampie/","flag_code": ""},
|
||||||
|
{"id": "43","name": "外遇","link": "/zh/cuckold/","flag_code": ""},
|
||||||
|
{"id": "241","name": "口腔内射精","link": "/zh/cum-in-mouth/","flag_code": ""},
|
||||||
|
{"id": "336","name": "阴道内射精","link": "/zh/cum-in-pussy/","flag_code": ""},
|
||||||
|
{"id": "339","name": "射精","link": "/zh/cumshot/","flag_code": ""},
|
||||||
|
{"id": "1927","name": "曲线","link": "/zh/curvy/","flag_code": ""},
|
||||||
|
{"id": "2031","name": "可爱","link": "/zh/cute/","flag_code": ""},
|
||||||
|
{"id": "368","name": "深喉","link": "/zh/deepthroat/","flag_code": ""},
|
||||||
|
{"id": "341","name": "假阳具","link": "/zh/dildo/","flag_code": ""},
|
||||||
|
{"id": "129","name": "后入式","link": "/zh/doggystyle/","flag_code": ""},
|
||||||
|
{"id": "251","name": "双龙","link": "/zh/double-penetration/","flag_code": ""},
|
||||||
|
{"id": "1706","name": "连衣裙","link": "/zh/dress/","flag_code": ""},
|
||||||
|
{"id": "234","name": "黑美人","link": "/zh/ebonies/","flag_code": ""},
|
||||||
|
{"id": "359","name": "面部的","link": "/zh/facial/","flag_code": ""},
|
||||||
|
{"id": "347","name": "脚","link": "/zh/feet/","flag_code": ""},
|
||||||
|
{"id": "348","name": "女性主导","link": "/zh/femdom/","flag_code": ""},
|
||||||
|
{"id": "2017","name": "菲律宾人","link": "/zh/filipina/","flag_code": "ph"},
|
||||||
|
{"id": "272","name": "猛攻","link": "/zh/gangbang/","flag_code": ""},
|
||||||
|
{"id": "23","name": "同性恋","link": "/zh/gay/","flag_code": "yy"},
|
||||||
|
{"id": "267","name": "玻璃","link": "/zh/glasses/","flag_code": ""},
|
||||||
|
{"id": "228","name": "奶奶","link": "/zh/granny/","flag_code": ""},
|
||||||
|
{"id": "1711","name": "健身房","link": "/zh/gym/","flag_code": ""},
|
||||||
|
{"id": "227","name": "多毛","link": "/zh/hairy/","flag_code": ""},
|
||||||
|
{"id": "353","name": "手冲","link": "/zh/handjob/","flag_code": ""},
|
||||||
|
{"id": "245","name": "硬核","link": "/zh/hardcore/","flag_code": ""},
|
||||||
|
{"id": "3090","name": "仙台","link": "/zh/hentai/","flag_code": ""},
|
||||||
|
{"id": "333","name": "高跟鞋","link": "/zh/high-heels/","flag_code": ""},
|
||||||
|
{"id": "314","name": "印度人","link": "/zh/indian/","flag_code": "in"},
|
||||||
|
{"id": "355","name": "日本人","link": "/zh/japanese/","flag_code": "jp"},
|
||||||
|
{"id": "25","name": "韩国人","link": "/zh/korean/","flag_code": "kr"},
|
||||||
|
{"id": "3","name": "跨性别","link": "/zh/ladyboy/","flag_code": "xy"},
|
||||||
|
{"id": "345","name": "拉丁美洲人","link": "/zh/latina/","flag_code": ""},
|
||||||
|
{"id": "229","name": "女同性恋","link": "/zh/lesbian/","flag_code": "xx"},
|
||||||
|
{"id": "330","name": "女士内衣","link": "/zh/lingerie/","flag_code": ""},
|
||||||
|
{"id": "239","name": "MILF","link": "/zh/milf/","flag_code": ""},
|
||||||
|
{"id": "346","name": "女仆","link": "/zh/maid/","flag_code": ""},
|
||||||
|
{"id": "277","name": "按摩","link": "/zh/massage/","flag_code": ""},
|
||||||
|
{"id": "290","name": "手淫","link": "/zh/masturbation/","flag_code": ""},
|
||||||
|
{"id": "326","name": "成熟","link": "/zh/mature/","flag_code": ""},
|
||||||
|
{"id": "278","name": "传教士","link": "/zh/missionary/","flag_code": ""},
|
||||||
|
{"id": "2095","name": "模特","link": "/zh/model/","flag_code": ""},
|
||||||
|
{"id": "201","name": "妈妈","link": "/zh/mom/","flag_code": ""},
|
||||||
|
{"id": "29","name": "天然山雀","link": "/zh/natural-tits/","flag_code": ""},
|
||||||
|
{"id": "2540","name": "修女","link": "/zh/nun/","flag_code": ""},
|
||||||
|
{"id": "306","name": "护士","link": "/zh/nurse/","flag_code": ""},
|
||||||
|
{"id": "356","name": "办公室","link": "/zh/office/","flag_code": ""},
|
||||||
|
{"id": "3046","name": "性高潮","link": "/zh/orgasm/","flag_code": ""},
|
||||||
|
{"id": "337","name": "戶外的","link": "/zh/outdoor/","flag_code": ""},
|
||||||
|
{"id": "257","name": "第一视角","link": "/zh/pov/","flag_code": ""},
|
||||||
|
{"id": "259","name": "童裤","link": "/zh/panties/","flag_code": ""},
|
||||||
|
{"id": "279","name": "连裤袜","link": "/zh/pantyhose/","flag_code": ""},
|
||||||
|
{"id": "264","name": "娇小","link": "/zh/petite/","flag_code": ""},
|
||||||
|
{"id": "1906","name": "菲律宾人XXX","link": "/zh/pinay/","flag_code": ""},
|
||||||
|
{"id": "283","name": "撒尿","link": "/zh/pissing/","flag_code": ""},
|
||||||
|
{"id": "243","name": "色情明星","link": "/zh/pornstar/","flag_code": ""},
|
||||||
|
{"id": "260","name": "怀孕","link": "/zh/pregnant/","flag_code": ""},
|
||||||
|
{"id": "298","name": "公開地","link": "/zh/public/","flag_code": ""},
|
||||||
|
{"id": "238","name": "阴户","link": "/zh/pussy/","flag_code": ""},
|
||||||
|
{"id": "322","name": "猫咪舔","link": "/zh/pussy-licking/","flag_code": ""},
|
||||||
|
{"id": "246","name": "红头发","link": "/zh/redhead/","flag_code": ""},
|
||||||
|
{"id": "5","name": "俄罗斯人","link": "/zh/russian/","flag_code": "ru"},
|
||||||
|
{"id": "225","name": "下垂的乳头","link": "/zh/saggy-tits/","flag_code": ""},
|
||||||
|
{"id": "270","name": "女学生","link": "/zh/schoolgirl/","flag_code": ""},
|
||||||
|
{"id": "325","name": "自拍","link": "/zh/selfie/","flag_code": ""},
|
||||||
|
{"id": "1775","name": "性爱娃娃","link": "/zh/sex-doll/","flag_code": ""},
|
||||||
|
{"id": "2565","name": "性感","link": "/zh/sexy/","flag_code": ""},
|
||||||
|
{"id": "1","name": "人妖","link": "/zh/shemale/","flag_code": "xy"},
|
||||||
|
{"id": "292","name": "短发","link": "/zh/short-hair/","flag_code": ""},
|
||||||
|
{"id": "305","name": "淋浴器","link": "/zh/shower/","flag_code": ""},
|
||||||
|
{"id": "249","name": "极瘦","link": "/zh/skinny/","flag_code": ""},
|
||||||
|
{"id": "236","name": "短裙","link": "/zh/skirt/","flag_code": ""},
|
||||||
|
{"id": "1827","name": "獨奏","link": "/zh/solo/","flag_code": ""},
|
||||||
|
{"id": "1880","name": "继姐","link": "/zh/step-sister/","flag_code": ""},
|
||||||
|
{"id": "1828","name": "继母","link": "/zh/stepmom/","flag_code": ""},
|
||||||
|
{"id": "247","name": "长筒袜","link": "/zh/stockings/","flag_code": ""},
|
||||||
|
{"id": "256","name": "高","link": "/zh/tall/","flag_code": ""},
|
||||||
|
{"id": "311","name": "纹身","link": "/zh/tattoo/","flag_code": ""},
|
||||||
|
{"id": "281","name": "教师","link": "/zh/teacher/","flag_code": ""},
|
||||||
|
{"id": "237","name": "青少年","link": "/zh/teens/","flag_code": ""},
|
||||||
|
{"id": "265","name": "泰国人","link": "/zh/thai/","flag_code": "th"},
|
||||||
|
{"id": "2032","name": "厚","link": "/zh/thick/","flag_code": ""},
|
||||||
|
{"id": "230","name": "三人","link": "/zh/threesome/","flag_code": ""},
|
||||||
|
{"id": "934","name": "乌克兰人","link": "/zh/ukrainian/","flag_code": "ua"},
|
||||||
|
{"id": "273","name": "裙底风光","link": "/zh/upskirt/","flag_code": ""},
|
||||||
|
{"id": "61","name": "古典","link": "/zh/vintage/","flag_code": ""},
|
||||||
|
{"id": "2544","name": "白人","link": "/zh/white/","flag_code": ""},
|
||||||
|
{"id": "2775","name": "瑜伽","link": "/zh/yoga/","flag_code": ""},
|
||||||
|
{"id": "275","name": "瑜伽裤","link": "/zh/yoga-pants/","flag_code": ""}
|
||||||
|
]
|
1302
src/all/pornpics/assets/i18n/channels_en.json
Normal file
1302
src/all/pornpics/assets/i18n/channels_en.json
Normal file
File diff suppressed because it is too large
Load Diff
1302
src/all/pornpics/assets/i18n/channels_zh.json
Normal file
1302
src/all/pornpics/assets/i18n/channels_zh.json
Normal file
File diff suppressed because it is too large
Load Diff
40
src/all/pornpics/assets/i18n/messages_en.properties
Normal file
40
src/all/pornpics/assets/i18n/messages_en.properties
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
chapter.name.default=Gallery
|
||||||
|
config-category-title=Categories
|
||||||
|
config-category-summary=Categories for popular and recently updated lists
|
||||||
|
config-category-option-default=Default
|
||||||
|
config-category-option-asian=Asian
|
||||||
|
config-category-option-chinese=Chinese
|
||||||
|
config-category-option-korean=Korean
|
||||||
|
config-category-option-japanese=Japanese
|
||||||
|
config-category-option-russian=Russian
|
||||||
|
config-category-option-ukrainian=Ukrainian
|
||||||
|
config-category-option-big-tits=Big tits
|
||||||
|
config-category-option-natural-tits=Natural tits
|
||||||
|
config-category-option-cosplay=Cosplay
|
||||||
|
config-category-option-cute=Cute
|
||||||
|
config-category-option-glasses=Glasses
|
||||||
|
config-category-option-maid=Maid
|
||||||
|
config-category-option-nurse=Nurse
|
||||||
|
config-category-option-nun=Nun
|
||||||
|
config-category-option-stockings=Stockings
|
||||||
|
config-category-option-twins=Twins
|
||||||
|
|
||||||
|
filter.header.ignored-when-search=These filters will be ignored when text search is active!
|
||||||
|
filter.header.select-active-category-type=Set activate category
|
||||||
|
filter.header.select-category-type-param=Set category parameters
|
||||||
|
|
||||||
|
filter.time.title=sort
|
||||||
|
filter.time.option.popular=Most Popular
|
||||||
|
filter.time.option.recent=Most Recent
|
||||||
|
|
||||||
|
filter.active-category-type.title=Active categories
|
||||||
|
filter.active-category-type.option.recommend=Recommend
|
||||||
|
filter.active-category-type.option.categories=Categories
|
||||||
|
filter.active-category-type.option.tags=Tags
|
||||||
|
filter.active-category-type.option.porn-star=Porn Star
|
||||||
|
filter.active-category-type.option.channels=Channels
|
||||||
|
filter.category-type.recommend.title=Recommend
|
||||||
|
filter.category-type.categories.title=Categories
|
||||||
|
filter.category-type.tags.title=Tags
|
||||||
|
filter.category-type.porn-star.title=Porn Star
|
||||||
|
filter.category-type.channels.title=Channels
|
40
src/all/pornpics/assets/i18n/messages_zh.properties
Normal file
40
src/all/pornpics/assets/i18n/messages_zh.properties
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
chapter.name.default=画廊
|
||||||
|
config-category-title=分类
|
||||||
|
config-category-summary=分类用于热门和最近更新列表
|
||||||
|
config-category-option-default=默认
|
||||||
|
config-category-option-asian=亚洲人
|
||||||
|
config-category-option-chinese=中国人
|
||||||
|
config-category-option-korean=韩国人
|
||||||
|
config-category-option-japanese=日本人
|
||||||
|
config-category-option-russian=俄罗斯人
|
||||||
|
config-category-option-ukrainian=乌克兰人
|
||||||
|
config-category-option-big-tits=巨乳
|
||||||
|
config-category-option-natural-tits=天然巨乳
|
||||||
|
config-category-option-cosplay=角色扮演
|
||||||
|
config-category-option-cute=可爱
|
||||||
|
config-category-option-glasses=眼镜
|
||||||
|
config-category-option-maid=女仆
|
||||||
|
config-category-option-nurse=护士
|
||||||
|
config-category-option-nun=修女
|
||||||
|
config-category-option-stockings=丝袜
|
||||||
|
config-category-option-twins=双胞胎
|
||||||
|
|
||||||
|
filter.header.ignored-when-search=以下过滤在文本搜索时会被忽略!
|
||||||
|
filter.header.select-active-category-type=选择激活分类类型
|
||||||
|
filter.header.select-category-type-param=选择分类类型参数
|
||||||
|
|
||||||
|
filter.time.title=排序
|
||||||
|
filter.time.option.popular=最受欢迎
|
||||||
|
filter.time.option.recent=最近更新
|
||||||
|
|
||||||
|
filter.active-category-type.title=激活分类类型
|
||||||
|
filter.active-category-type.option.recommend=推荐
|
||||||
|
filter.active-category-type.option.categories=分类
|
||||||
|
filter.active-category-type.option.tags=标签
|
||||||
|
filter.active-category-type.option.porn-star=色情明星
|
||||||
|
filter.active-category-type.option.channels=频道
|
||||||
|
filter.category-type.recommend.title=推荐
|
||||||
|
filter.category-type.categories.title=分类
|
||||||
|
filter.category-type.tags.title=标签
|
||||||
|
filter.category-type.porn-star.title=色情明星
|
||||||
|
filter.category-type.channels.title=频道
|
2537
src/all/pornpics/assets/i18n/porn_stars_en.json
Normal file
2537
src/all/pornpics/assets/i18n/porn_stars_en.json
Normal file
File diff suppressed because it is too large
Load Diff
2537
src/all/pornpics/assets/i18n/porn_stars_zh.json
Normal file
2537
src/all/pornpics/assets/i18n/porn_stars_zh.json
Normal file
File diff suppressed because it is too large
Load Diff
30
src/all/pornpics/assets/i18n/recommend_categories_en.json
Normal file
30
src/all/pornpics/assets/i18n/recommend_categories_en.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{"name":"Asian","categoryTypeName": "CATEGORY","link": "/asian"},
|
||||||
|
{"name":"Chinese","categoryTypeName": "CATEGORY","link": "/chinese"},
|
||||||
|
{"name":"Korean","categoryTypeName": "CATEGORY","link": "/korean"},
|
||||||
|
{"name":"Japanese","categoryTypeName": "CATEGORY","link": "/japanese"},
|
||||||
|
{"name":"Russian","categoryTypeName": "CATEGORY","link": "/russian"},
|
||||||
|
{"name":"Ukrainian","categoryTypeName": "CATEGORY","link": "/ukrainian"},
|
||||||
|
{"name":"Big tits","categoryTypeName": "CATEGORY","link": "/big-tits"},
|
||||||
|
{"name":"Natural tits","categoryTypeName": "CATEGORY","link": "/natural-tits"},
|
||||||
|
{"name":"Cosplay","categoryTypeName": "CATEGORY","link": "/cosplay"},
|
||||||
|
{"name":"Cute","categoryTypeName": "CATEGORY","link": "/cute"},
|
||||||
|
{"name":"Glasses","categoryTypeName": "CATEGORY","link": "/glasses"},
|
||||||
|
{"name":"Maid","categoryTypeName": "CATEGORY","link": "/maid"},
|
||||||
|
{"name":"Nurse","categoryTypeName": "CATEGORY","link": "/nurse"},
|
||||||
|
{"name":"Nun","categoryTypeName": "CATEGORY","link": "/nun"},
|
||||||
|
{"name":"Stockings","categoryTypeName": "CATEGORY","link": "/stockings"},
|
||||||
|
{"name":"Twins","categoryTypeName": "CATEGORY","link": "/twins"},
|
||||||
|
|
||||||
|
{"name": "Schoolgirl", "categoryTypeName": "CATEGORY", "link": "/schoolgirl/"},
|
||||||
|
{"name": "Cowgirl", "categoryTypeName": "CATEGORY", "link": "/cowgirl/"},
|
||||||
|
{"name": "Beautiful", "categoryTypeName": "CATEGORY", "link": "/beautiful/"},
|
||||||
|
{"name": "Orgasm", "categoryTypeName": "CATEGORY", "link": "/orgasm/"},
|
||||||
|
{"name": "High Heels", "categoryTypeName": "CATEGORY", "link": "/high-heels/"},
|
||||||
|
|
||||||
|
{"name": "Onlyfans", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=onlyfans"},
|
||||||
|
{"name": "Patreon koreanbeautygirl", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+koreanbeautygirl"},
|
||||||
|
{"name": "Patreon Hornycos", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+hornycos"},
|
||||||
|
{"name": "Patreon Scarletai", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+scarletai"}
|
||||||
|
|
||||||
|
]
|
29
src/all/pornpics/assets/i18n/recommend_categories_zh.json
Normal file
29
src/all/pornpics/assets/i18n/recommend_categories_zh.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[
|
||||||
|
{"name": "亚洲人", "categoryTypeName": "CATEGORY", "link": "/zh/asian"},
|
||||||
|
{"name": "中国人", "categoryTypeName": "CATEGORY", "link": "/zh/chinese"},
|
||||||
|
{"name": "韩国人", "categoryTypeName": "CATEGORY", "link": "/zh/korean"},
|
||||||
|
{"name": "日本人", "categoryTypeName": "CATEGORY", "link": "/zh/japanese"},
|
||||||
|
{"name": "俄罗斯人", "categoryTypeName": "CATEGORY", "link": "/zh/russian"},
|
||||||
|
{"name": "乌克兰人", "categoryTypeName": "CATEGORY", "link": "/zh/ukrainian"},
|
||||||
|
{"name": "巨乳", "categoryTypeName": "CATEGORY", "link": "/zh/big-tits"},
|
||||||
|
{"name": "天然巨乳", "categoryTypeName": "CATEGORY", "link": "/zh/natural-tits"},
|
||||||
|
{"name": "角色扮演", "categoryTypeName": "CATEGORY", "link": "/zh/cosplay"},
|
||||||
|
{"name": "可爱", "categoryTypeName": "CATEGORY", "link": "/zh/cute"},
|
||||||
|
{"name": "眼镜", "categoryTypeName": "CATEGORY", "link": "/zh/glasses"},
|
||||||
|
{"name": "女仆", "categoryTypeName": "CATEGORY", "link": "/zh/maid"},
|
||||||
|
{"name": "护士", "categoryTypeName": "CATEGORY", "link": "/zh/nurse"},
|
||||||
|
{"name": "修女", "categoryTypeName": "CATEGORY", "link": "/zh/nun"},
|
||||||
|
{"name": "丝袜", "categoryTypeName": "CATEGORY", "link": "/zh/stockings"},
|
||||||
|
{"name": "双胞胎", "categoryTypeName": "CATEGORY", "link": "/zh/twins"},
|
||||||
|
|
||||||
|
{"name": "女学生", "categoryTypeName": "CATEGORY", "link": "/zh/schoolgirl/"},
|
||||||
|
{"name": "女牛仔", "categoryTypeName": "CATEGORY", "link": "/zh/cowgirl/"},
|
||||||
|
{"name": "美丽", "categoryTypeName": "CATEGORY", "link": "/zh/beautiful/"},
|
||||||
|
{"name": "性高潮", "categoryTypeName": "CATEGORY", "link": "/zh/orgasm/"},
|
||||||
|
{"name": "高跟鞋", "categoryTypeName": "CATEGORY", "link": "/zh/high-heels/"},
|
||||||
|
|
||||||
|
{"name": "Onlyfans", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=onlyfans"},
|
||||||
|
{"name": "Patreon 韩国美女", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+koreanbeautygirl"},
|
||||||
|
{"name": "Patreon Horny COS", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+hornycos"},
|
||||||
|
{"name": "Patreon Scarlet AI", "categoryTypeName": "CHANNEL", "link": "/search/srch.php?q=patreon+scarletai"}
|
||||||
|
]
|
3321
src/all/pornpics/assets/i18n/tags_en.json
Normal file
3321
src/all/pornpics/assets/i18n/tags_en.json
Normal file
File diff suppressed because it is too large
Load Diff
3321
src/all/pornpics/assets/i18n/tags_zh.json
Normal file
3321
src/all/pornpics/assets/i18n/tags_zh.json
Normal file
File diff suppressed because it is too large
Load Diff
12
src/all/pornpics/build.gradle
Normal file
12
src/all/pornpics/build.gradle
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'PornPics'
|
||||||
|
extClass = '.PornPics'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:i18n'))
|
||||||
|
}
|
BIN
src/all/pornpics/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/pornpics/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
src/all/pornpics/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/pornpics/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
src/all/pornpics/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/pornpics/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
BIN
src/all/pornpics/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/pornpics/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.pornpics
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import okio.FileNotFoundException
|
||||||
|
import okio.IOException
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
object JsonFileLoader {
|
||||||
|
val json: Json by injectLazy()
|
||||||
|
val classLoader = this::class.java.classLoader!!
|
||||||
|
|
||||||
|
inline fun <reified T> loadJsonAs(fileName: String): T {
|
||||||
|
val fileContent = classLoader.getResourceAsStream(fileName)
|
||||||
|
?: throw FileNotFoundException("Cannot find JSON file: $fileName")
|
||||||
|
|
||||||
|
try {
|
||||||
|
return json.decodeFromStream<T>(fileContent)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
throw IllegalArgumentException("Failed to parse JSON file: $fileName", e)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw IOException("Failed to read JSON file: $fileName", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> loadLangJsonAs(name: String, lang: String): T {
|
||||||
|
val fileName = "assets/i18n/${name}_$lang.json"
|
||||||
|
return loadJsonAs<T>(fileName)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.pornpics
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.firstInstance
|
||||||
|
import keiyoushi.utils.getPreferences
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class PornPics() : SimpleParsedHttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
|
override val baseUrl = "https://www.pornpics.com"
|
||||||
|
override val lang = "all"
|
||||||
|
override val name = "PornPics"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val preferences = getPreferences()
|
||||||
|
|
||||||
|
private val intl = Intl(
|
||||||
|
language = lang,
|
||||||
|
baseLanguage = "en",
|
||||||
|
availableLanguages = setOf("en", "zh"),
|
||||||
|
classLoader = this::class.java.classLoader!!,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
PornPicsPreferences.buildPreferences(screen.context, intl).forEach(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun simpleMangaSelector() = "#main li.thumbwook > a.rel-link"
|
||||||
|
|
||||||
|
override fun simpleMangaFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class MangaDto(
|
||||||
|
val desc: String,
|
||||||
|
@SerialName("g_url")
|
||||||
|
val url: String,
|
||||||
|
@SerialName("t_url")
|
||||||
|
val thumbnailUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun simpleMangaParse(response: Response): MangasPage {
|
||||||
|
// the default list is always JSON,
|
||||||
|
// the search list is always JSON,
|
||||||
|
// the first page of other classification lists is HTML, and other pages are JSON
|
||||||
|
val url = response.request.url
|
||||||
|
val isSearch = url.queryParameter("q") != null
|
||||||
|
val isDefault = url.queryParameter("period") != null
|
||||||
|
val offset = url.queryParameter("offset")!!.toInt()
|
||||||
|
val responseAsJson = isSearch || isDefault || offset > 0
|
||||||
|
|
||||||
|
val mangas = if (responseAsJson) {
|
||||||
|
val data = response.parseAs<List<MangaDto>>()
|
||||||
|
data.map {
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(it.url)
|
||||||
|
title = it.desc
|
||||||
|
thumbnail_url = it.thumbnailUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val doc = response.asJsoup()
|
||||||
|
doc.select(simpleMangaSelector()).map {
|
||||||
|
val imgEl = it.selectFirst("img")!!
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(it.absUrl("href"))
|
||||||
|
title = imgEl.attr("alt")
|
||||||
|
thumbnail_url = imgEl.absUrl("data-src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// response maybe [], Add +1 to requested image count per page,
|
||||||
|
// Compare actual received count with pageSize to determine next page.
|
||||||
|
val hasNextPage = mangas.size > QUERY_PAGE_SIZE
|
||||||
|
val readerMangas = if (hasNextPage) mangas.dropLast(1) else mangas
|
||||||
|
return MangasPage(readerMangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun simpleNextPageSelector(): String? = null
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = buildMangasPageRequest(page, true)
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = buildMangasPageRequest(page, false)
|
||||||
|
|
||||||
|
private fun buildMangasPageRequest(page: Int, popular: Boolean): Request {
|
||||||
|
val categoryOption = PornPicsPreferences.getCategoryOption(preferences)
|
||||||
|
if (PornPicsPreferences.DEFAULT_CATEGORY_OPTION == categoryOption) {
|
||||||
|
// the source of is the options under the pics menu in the nav bar
|
||||||
|
val period = if (popular) 1 else 2
|
||||||
|
val categoryId = 2585 + period
|
||||||
|
return "$baseUrl/popular/api/galleries/list".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameterPage(page)
|
||||||
|
.addQueryParameter("lang", intl.chosenLanguage)
|
||||||
|
.addQueryParameter("period", period)
|
||||||
|
.addQueryParameter("category_id", categoryId)
|
||||||
|
.build()
|
||||||
|
.let { GET(it, headers) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// the source is the options under the categories/tags/pornstars/channels menu in the nav bar
|
||||||
|
val requestBaseUrl = if (popular) "$baseUrl/$categoryOption" else "$baseUrl/$categoryOption/recent"
|
||||||
|
return requestBaseUrl.toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameterPage(page)
|
||||||
|
.addQueryParameter("lang", intl.chosenLanguage)
|
||||||
|
.build()
|
||||||
|
.let { GET(it, headers) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val thumbEl = document.selectFirst(simpleMangaSelector())!!
|
||||||
|
val imgEl = thumbEl.selectFirst("img")!!
|
||||||
|
val infoEl = document.selectFirst("div.gallery-info.to-gall-info")
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = imgEl.attr("alt")
|
||||||
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
status = SManga.COMPLETED
|
||||||
|
author = infoEl?.select("div.gallery-info__item:nth-child(2) a")?.joinToString { it.text() }
|
||||||
|
genre = infoEl?.select("div.gallery-info__item:not(:nth-child(2)) a")?.joinToString { it.text() }
|
||||||
|
description = infoEl?.selectFirst("div.gallery-info__item:nth-child(4)")?.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return Observable.just(manga)
|
||||||
|
.map {
|
||||||
|
SChapter.create().apply {
|
||||||
|
chapter_number = 0F
|
||||||
|
setUrlWithoutDomain(it.url)
|
||||||
|
name = intl["chapter.name.default"]
|
||||||
|
}.let { listOf(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select(simpleMangaSelector())
|
||||||
|
.mapIndexed { index, element ->
|
||||||
|
Page(index, imageUrl = element.absUrl("href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
return if (query.isBlank()) {
|
||||||
|
buildCategoryRequest(page, filters)
|
||||||
|
} else {
|
||||||
|
buildSearchRequest(page, query, filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCategoryRequest(
|
||||||
|
page: Int,
|
||||||
|
filters: FilterList,
|
||||||
|
): Request {
|
||||||
|
val activeCategoryTypeOption = filters.firstInstance<PornPicsFilters.ActiveCategoryTypeSelector>()
|
||||||
|
val categoryOption = activeCategoryTypeOption.selectedCategoryOption(filters)
|
||||||
|
val sortOption = filters.firstInstance<PornPicsFilters.SortSelector>()
|
||||||
|
val builder = baseUrl.toHttpUrl().newBuilder()
|
||||||
|
.addUrlPart(categoryOption.toUrlPart())
|
||||||
|
.addQueryParameter("lang", intl.chosenLanguage)
|
||||||
|
.addUrlPart(sortOption.toUriPart(), addPath = !categoryOption.useSearch())
|
||||||
|
.addQueryParameterPage(page)
|
||||||
|
return GET(builder.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSearchRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val sortOption = filters.firstInstance<PornPicsFilters.SortSelector>()
|
||||||
|
val builder = "$baseUrl/search/srch.php".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("lang", intl.chosenLanguage)
|
||||||
|
.addUrlPart(sortOption.toUriPart(), addPath = false)
|
||||||
|
.addQueryParameterPage(page)
|
||||||
|
.addQueryParameter("q", query)
|
||||||
|
return GET(builder.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
PornPicsFilters.createSortSelector(intl),
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header(intl["filter.header.ignored-when-search"]),
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header(intl["filter.header.select-active-category-type"]),
|
||||||
|
PornPicsFilters.createActiveCategoryTypeSelector(intl),
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header(intl["filter.header.select-category-type-param"]),
|
||||||
|
PornPicsFilters.createRecommendSelector(intl),
|
||||||
|
PornPicsFilters.createCategorySelector(intl),
|
||||||
|
PornPicsFilters.createTagSelector(intl),
|
||||||
|
PornPicsFilters.createPornStarSelector(intl),
|
||||||
|
PornPicsFilters.createChannelSelector(intl),
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.pornpics
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
|
||||||
|
internal const val QUERY_PAGE_SIZE = 19
|
||||||
|
|
||||||
|
internal fun HttpUrl.Builder.addQueryParameter(encodedName: String, encodedValue: Int) =
|
||||||
|
addQueryParameter(encodedName, encodedValue.toString())
|
||||||
|
|
||||||
|
internal fun HttpUrl.Builder.addQueryParameterPage(page: Int) =
|
||||||
|
// Add +1 to requested image count per page,
|
||||||
|
// Compare actual received count with pageSize to determine next page.
|
||||||
|
this.addQueryParameter("limit", QUERY_PAGE_SIZE + 1)
|
||||||
|
.addQueryParameter("offset", (page - 1) * QUERY_PAGE_SIZE)
|
||||||
|
|
||||||
|
internal fun HttpUrl.Builder.addUrlPart(
|
||||||
|
urlPart: String?,
|
||||||
|
addPath: Boolean = true,
|
||||||
|
addQuery: Boolean = true,
|
||||||
|
): HttpUrl.Builder {
|
||||||
|
if (urlPart == null) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
val httpUrl = "https://fake.com/$urlPart".toHttpUrl()
|
||||||
|
if (addPath) {
|
||||||
|
httpUrl.pathSegments.forEach { addPathSegment(it) }
|
||||||
|
}
|
||||||
|
if (addQuery) {
|
||||||
|
httpUrl.queryParameterNames.forEach { name ->
|
||||||
|
httpUrl.queryParameterValues(name).forEach { value ->
|
||||||
|
addQueryParameter(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addUrlPart(part: String) {
|
||||||
|
"https://www.baidu.com/01/?ppppp=5&".toHttpUrl().newBuilder()
|
||||||
|
.addUrlPart(part)
|
||||||
|
.build()
|
||||||
|
.let { println(it) }
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.pornpics
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class PornPicsFilters {
|
||||||
|
|
||||||
|
data class SortOption(val name: String, val urlPart: String? = null) {
|
||||||
|
override fun toString() = this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortSelector(name: String, private val vals: Array<SortOption>) :
|
||||||
|
Filter.Select<SortOption>(name, vals) {
|
||||||
|
fun toUriPart() = vals[state].urlPart
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CategoryType() {
|
||||||
|
RECOMMEND,
|
||||||
|
CATEGORY,
|
||||||
|
TAG,
|
||||||
|
PORN_STAR,
|
||||||
|
CHANNEL,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val mNameToCategoryType = CategoryType.values().associateBy { it.name.lowercase(Locale.US) }
|
||||||
|
|
||||||
|
fun of(name: String) = mNameToCategoryType[name.lowercase(Locale.US)]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CategoryOption(val name: String, val type: CategoryType, val urlPart: String) {
|
||||||
|
override fun toString() = this.name
|
||||||
|
fun toUrlPart() = urlPart
|
||||||
|
fun useSearch() = urlPart.contains("/search/srch.php?")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ActiveCategoryOption(val name: String, val categoryType: CategoryType?) {
|
||||||
|
override fun toString() = this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActiveCategoryTypeSelector(name: String, values: Array<ActiveCategoryOption>) :
|
||||||
|
Filter.Select<ActiveCategoryOption>(name, values) {
|
||||||
|
fun selected() = values[state].categoryType!!
|
||||||
|
fun selectedCategoryOption(filters: FilterList): CategoryOption {
|
||||||
|
val selectors = filters.filterIsInstance<CategorySelector>()
|
||||||
|
val selected = selected()
|
||||||
|
return selectors[selected.ordinal].selected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategorySelector(name: String, values: Array<CategoryOption>) : Filter.Select<CategoryOption>(name, values) {
|
||||||
|
fun selected() = values[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createSortSelector(intl: Intl) = SortSelector(
|
||||||
|
intl["filter.time.title"],
|
||||||
|
arrayOf(
|
||||||
|
SortOption(intl["filter.time.option.popular"]),
|
||||||
|
SortOption(intl["filter.time.option.recent"], "recent?date=latest"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun createActiveCategoryTypeSelector(intl: Intl) = ActiveCategoryTypeSelector(
|
||||||
|
intl["filter.active-category-type.title"],
|
||||||
|
arrayOf(
|
||||||
|
ActiveCategoryOption(intl["filter.active-category-type.option.recommend"], CategoryType.RECOMMEND),
|
||||||
|
ActiveCategoryOption(intl["filter.active-category-type.option.categories"], CategoryType.CATEGORY),
|
||||||
|
ActiveCategoryOption(intl["filter.active-category-type.option.tags"], CategoryType.TAG),
|
||||||
|
ActiveCategoryOption(intl["filter.active-category-type.option.porn-star"], CategoryType.PORN_STAR),
|
||||||
|
ActiveCategoryOption(intl["filter.active-category-type.option.channels"], CategoryType.CHANNEL),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RecommendCategoryDto(val name: String, val categoryTypeName: String, val link: String)
|
||||||
|
|
||||||
|
fun createRecommendSelector(intl: Intl): CategorySelector {
|
||||||
|
val options = JsonFileLoader.loadLangJsonAs<List<RecommendCategoryDto>>(
|
||||||
|
"recommend_categories",
|
||||||
|
intl.chosenLanguage,
|
||||||
|
)
|
||||||
|
.map { CategoryOption(it.name, CategoryType.of(it.categoryTypeName), it.link) }
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.toTypedArray()
|
||||||
|
return CategorySelector(
|
||||||
|
intl["filter.category-type.recommend.title"],
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CategoryDto(val name: String, val link: String)
|
||||||
|
|
||||||
|
fun createCategorySelector(intl: Intl): CategorySelector {
|
||||||
|
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("categories", intl.chosenLanguage)
|
||||||
|
.map { CategoryOption(it.name, CategoryType.CATEGORY, it.link) }
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
return CategorySelector(
|
||||||
|
intl["filter.category-type.categories.title"],
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTagSelector(intl: Intl): CategorySelector {
|
||||||
|
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("tags", intl.chosenLanguage)
|
||||||
|
.map { CategoryOption(it.name, CategoryType.TAG, it.link) }
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.toTypedArray()
|
||||||
|
|
||||||
|
return CategorySelector(
|
||||||
|
intl["filter.category-type.tags.title"],
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPornStarSelector(intl: Intl): CategorySelector {
|
||||||
|
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("porn_stars", intl.chosenLanguage)
|
||||||
|
.map { CategoryOption(it.name, CategoryType.PORN_STAR, it.link) }
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.toTypedArray()
|
||||||
|
return CategorySelector(
|
||||||
|
intl["filter.category-type.porn-star.title"],
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createChannelSelector(intl: Intl): CategorySelector {
|
||||||
|
val options = JsonFileLoader.loadLangJsonAs<List<CategoryDto>>("channels", intl.chosenLanguage)
|
||||||
|
.map { CategoryOption(it.name, CategoryType.CHANNEL, it.link) }
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.toTypedArray()
|
||||||
|
return CategorySelector(
|
||||||
|
intl["filter.category-type.channels.title"],
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.pornpics
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
|
|
||||||
|
object PornPicsPreferences {
|
||||||
|
|
||||||
|
private const val PS_KEY_ROOT = "PornPics"
|
||||||
|
|
||||||
|
private const val PS_KEY_CATEGORY = "$PS_KEY_ROOT::CATEGORY"
|
||||||
|
|
||||||
|
const val DEFAULT_CATEGORY_OPTION = "/default"
|
||||||
|
|
||||||
|
private fun buildCategoryOption(intl: Intl): Map<String, String> {
|
||||||
|
return linkedMapOf(
|
||||||
|
intl["config-category-option-default"] to DEFAULT_CATEGORY_OPTION,
|
||||||
|
intl["config-category-option-asian"] to "asian",
|
||||||
|
intl["config-category-option-chinese"] to "chinese",
|
||||||
|
intl["config-category-option-korean"] to "korean",
|
||||||
|
intl["config-category-option-japanese"] to "japanese",
|
||||||
|
intl["config-category-option-russian"] to "russian",
|
||||||
|
intl["config-category-option-ukrainian"] to "ukrainian",
|
||||||
|
intl["config-category-option-big-tits"] to "big-tits",
|
||||||
|
intl["config-category-option-natural-tits"] to "natural-tits",
|
||||||
|
intl["config-category-option-cosplay"] to "cosplay",
|
||||||
|
intl["config-category-option-cute"] to "cute",
|
||||||
|
intl["config-category-option-glasses"] to "glasses",
|
||||||
|
intl["config-category-option-maid"] to "maid",
|
||||||
|
intl["config-category-option-nurse"] to "nurse",
|
||||||
|
intl["config-category-option-nun"] to "nun",
|
||||||
|
intl["config-category-option-stockings"] to "stockings",
|
||||||
|
intl["config-category-option-twins"] to "twins",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildPreferences(content: Context, intl: Intl): List<ListPreference> {
|
||||||
|
val options = buildCategoryOption(intl)
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
ListPreference(content).apply {
|
||||||
|
key = PS_KEY_CATEGORY
|
||||||
|
title = intl["config-category-title"]
|
||||||
|
summary = intl["config-category-summary"]
|
||||||
|
entries = options.keys.toTypedArray()
|
||||||
|
entryValues = options.values.toTypedArray()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCategoryOption(sharedPreferences: SharedPreferences): String? {
|
||||||
|
val option = sharedPreferences.getString(PS_KEY_CATEGORY, DEFAULT_CATEGORY_OPTION)
|
||||||
|
return if (DEFAULT_CATEGORY_OPTION == option) DEFAULT_CATEGORY_OPTION else option
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.pornpics
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
abstract class SimpleParsedHttpSource : ParsedHttpSource() {
|
||||||
|
|
||||||
|
abstract fun simpleMangaSelector(): String
|
||||||
|
|
||||||
|
abstract fun simpleMangaFromElement(element: Element): SManga
|
||||||
|
|
||||||
|
abstract fun simpleMangaParse(response: Response): MangasPage
|
||||||
|
|
||||||
|
abstract fun simpleNextPageSelector(): String?
|
||||||
|
|
||||||
|
// region popular
|
||||||
|
override fun popularMangaSelector() = simpleMangaSelector()
|
||||||
|
override fun popularMangaNextPageSelector() = simpleNextPageSelector()
|
||||||
|
override fun popularMangaParse(response: Response) = simpleMangaParse(response)
|
||||||
|
override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element)
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region last
|
||||||
|
override fun latestUpdatesSelector() = simpleMangaSelector()
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = simpleMangaFromElement(element)
|
||||||
|
override fun latestUpdatesParse(response: Response) = simpleMangaParse(response)
|
||||||
|
override fun latestUpdatesNextPageSelector() = simpleNextPageSelector()
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region search
|
||||||
|
override fun searchMangaSelector() = simpleMangaSelector()
|
||||||
|
override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element)
|
||||||
|
override fun searchMangaParse(response: Response) = simpleMangaParse(response)
|
||||||
|
override fun searchMangaNextPageSelector() = simpleNextPageSelector()
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
override fun chapterListSelector() = simpleMangaSelector()
|
||||||
|
override fun imageUrlParse(document: Document): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user