mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-20 17:01:24 -07:00
refactor: remove dead code and enable noUnusedLocals (#292)
* refactor: remove dead code and enable noUnusedLocals
- delete launcher/jimaku.ts and the launcher Args/config jimaku* fields
that only fed it (the SUBMINER_JIMAKU_* env vars had no effect)
- delete plugin/subminer/hover.lua; nothing has sent subminer-hover-token
since Overlay 2.0
- drop never-read anilist.characterDictionary.refreshTtlHours and
evictionPolicy (still hidden in the settings UI for old configs)
- drop unused root deps: eslint, @fontsource-variable/geist{,-mono}
(stats/ declares its own)
- delete the committed draft renderer-performance plan doc
- turn on noUnusedLocals for typecheck and remove what it surfaced,
including Windows focus-handoff values computed but never consumed
* docs(changes): add docs fragment and note retired hover messages
This commit is contained in:
1 parent
ea2b3a9507
commit
8fc20b2499
52 files changed
+20
-1683
No files matched your search
@@ -5,8 +5,6 @@
|
||||
"": {
|
||||
"name": "subminer",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@fontsource-variable/geist-mono": "^5.2.7",
|
||||
"@xhayper/discord-rpc": "^1.3.4",
|
||||
"axios": "^1.20.0",
|
||||
"commander": "^14.0.3",
|
||||
@@ -24,7 +22,6 @@
|
||||
"electron": "43.7.7",
|
||||
"electron-builder": "26.16.1",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^10.8.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"undici": "7.29.1",
|
||||
@@ -127,34 +124,6 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
|
||||
|
||||
"@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="],
|
||||
|
||||
"@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||
|
||||
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.28", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Lc/b8JXO2W2+H+5UXfw7PCHZCim1jlrB0CmLPsjfVmihMluBpdYafFImhjAHxHlWGfuZ32WzjVPUap5fGmkthw=="],
|
||||
@@ -203,16 +172,10 @@
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
|
||||
|
||||
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
@@ -231,13 +194,9 @@
|
||||
|
||||
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
@@ -317,8 +276,6 @@
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
@@ -383,44 +340,16 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
||||
@@ -441,8 +370,6 @@
|
||||
|
||||
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
@@ -475,20 +402,12 @@
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||
|
||||
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
||||
@@ -503,9 +422,7 @@
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||
|
||||
@@ -521,12 +438,8 @@
|
||||
|
||||
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
|
||||
|
||||
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
||||
@@ -563,8 +476,6 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="],
|
||||
|
||||
"node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
|
||||
@@ -581,16 +492,10 @@
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -607,8 +512,6 @@
|
||||
|
||||
"postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||
@@ -625,8 +528,6 @@
|
||||
|
||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||
|
||||
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
|
||||
@@ -711,8 +612,6 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
@@ -725,8 +624,6 @@
|
||||
|
||||
"unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
@@ -735,8 +632,6 @@
|
||||
|
||||
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
@@ -771,8 +666,6 @@
|
||||
|
||||
"@electron/windows-sign/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||
|
||||
"@types/cacheable-request/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
@@ -787,8 +680,6 @@
|
||||
|
||||
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
|
||||
|
||||
"app-builder-lib/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||
|
||||
"builder-util/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
@@ -835,8 +726,6 @@
|
||||
|
||||
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"app-builder-lib/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"builder-util/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
type: docs
|
||||
area: launcher
|
||||
|
||||
- Removed `SUBMINER_JIMAKU_API_KEY`, `SUBMINER_JIMAKU_API_KEY_COMMAND`, and `SUBMINER_JIMAKU_API_BASE_URL` from the development docs' environment variable table. The launcher never used them; set the Jimaku key with `jimaku.apiKey` / `jimaku.apiKeyCommand` in the config instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
type: internal
|
||||
area: cleanup
|
||||
|
||||
- Removed dead code: the unused launcher Jimaku client (the `SUBMINER_JIMAKU_*` env vars it read were documented but had no effect), the mpv plugin's hover-highlight module (no sender since Overlay 2.0; custom mpv scripts that still send `subminer-hover-token` or `yomipv-hover-token` are now ignored), the never-read `anilist.characterDictionary.refreshTtlHours` / `evictionPolicy` config keys, unused root dependencies (eslint, geist fonts), and a committed draft plan doc.
|
||||
- Typecheck now runs with `noUnusedLocals`; removed the unused locals, imports, and the Windows overlay focus-handoff values they fed, which were computed but never used.
|
||||
@@ -175,9 +175,6 @@ Run `make help` for the full list.
|
||||
| `SUBMINER_MPV_PLUGIN_PATH` | mpv plugin directory the launcher injects |
|
||||
| `SUBMINER_LOG_LEVEL` | App log level (`debug`, `info`, `warn`, `error`) |
|
||||
| `SUBMINER_MPV_LOG` | Shared mpv/app log file path |
|
||||
| `SUBMINER_JIMAKU_API_KEY` | Jimaku API key for launcher subtitle downloads |
|
||||
| `SUBMINER_JIMAKU_API_KEY_COMMAND` | Command that prints the Jimaku API key |
|
||||
| `SUBMINER_JIMAKU_API_BASE_URL` | Jimaku API base URL |
|
||||
| `SUBMINER_JELLYFIN_ACCESS_TOKEN` | Jellyfin access token, used before the stored encrypted session |
|
||||
| `SUBMINER_JELLYFIN_USER_ID` | Jellyfin user ID |
|
||||
| `SUBMINER_SKIP_MACOS_HELPER_BUILD` | Set to `1` to skip the macOS helper build during `bun run build` |
|
||||
@@ -1,298 +0,0 @@
|
||||
# Renderer Performance Optimizations
|
||||
|
||||
**Date:** 2026-03-15
|
||||
**Status:** Draft
|
||||
|
||||
## Goal
|
||||
|
||||
Minimize the time between a subtitle line appearing and annotations being displayed. Three optimizations target different pipeline stages to achieve this.
|
||||
|
||||
## Current Pipeline (Warm State)
|
||||
|
||||
```text
|
||||
MPV subtitle change (0ms)
|
||||
-> IPC to main (5ms)
|
||||
-> Cache check (2ms)
|
||||
-> [CACHE MISS] Yomitan parser (35-180ms)
|
||||
-> Parallel: MeCab enrichment (20-80ms) + Frequency lookup (15-50ms)
|
||||
-> Annotation stage: 4 sequential passes (25-70ms)
|
||||
-> IPC to renderer (10ms)
|
||||
-> DOM render: createElement per token (15-50ms)
|
||||
─────────────────────────────────
|
||||
Total: ~200-320ms (cache miss)
|
||||
Total: ~72ms (cache hit)
|
||||
```
|
||||
|
||||
## Target Pipeline
|
||||
|
||||
```text
|
||||
MPV subtitle change (0ms)
|
||||
-> IPC to main (5ms)
|
||||
-> Cache check (2ms)
|
||||
-> [CACHE HIT via prefetch] (0ms)
|
||||
-> IPC to renderer (10ms)
|
||||
-> DOM render: cloneNode from template (10-30ms)
|
||||
─────────────────────────────────
|
||||
Total: ~30-50ms (prefetch-warmed, normal playback)
|
||||
|
||||
[CACHE MISS, e.g. immediate seek]
|
||||
-> Yomitan parser (35-180ms)
|
||||
-> Parallel: MeCab enrichment + Frequency lookup
|
||||
-> Annotation stage: 1 batched pass (10-25ms)
|
||||
-> IPC to renderer (10ms)
|
||||
-> DOM render: cloneNode from template (10-30ms)
|
||||
─────────────────────────────────
|
||||
Total: ~150-260ms (cache miss, still improved)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optimization 1: Subtitle Prefetching
|
||||
|
||||
### Summary
|
||||
|
||||
A new `SubtitlePrefetchService` parses external subtitle files and tokenizes upcoming lines in the background before they appear on screen. This converts most cache misses into cache hits during normal playback.
|
||||
|
||||
### Scope
|
||||
|
||||
External subtitle files only (SRT, VTT, ASS). Embedded subtitle tracks are out of scope since Japanese subtitles are virtually always external files.
|
||||
|
||||
### Architecture
|
||||
|
||||
#### Subtitle File Parsing
|
||||
|
||||
A cue parser extracts both timing and text content from subtitle files for prefetching.
|
||||
|
||||
**Parsed cue structure:**
|
||||
|
||||
```typescript
|
||||
interface SubtitleCue {
|
||||
startTime: number; // seconds
|
||||
endTime: number; // seconds
|
||||
text: string; // plain text, decoded from the source format
|
||||
source?: 'canonical-ass'; // recovered authored text for generated ASS animation
|
||||
animationStartTime?: number; // full generated-frame envelope; entrance/exit frames
|
||||
animationEndTime?: number; // run past the authored timing, live matching uses this
|
||||
}
|
||||
```
|
||||
|
||||
**Supported formats:**
|
||||
|
||||
- SRT/VTT: Regex-based parsing of timing lines + text content between timing blocks.
|
||||
- ASS: Parse the `[Events]` section, read the field order from the `Format:` row, and extract timed `Dialogue:` lines. Timed `Comment:` lines are normally ignored, but can supply canonical authored text when they match a nearby generated animation from the same style and actor. Text can itself contain commas.
|
||||
|
||||
**ASS decoding.** The parser is where ASS text is decoded, once, via `assToPlainText()` in `src/core/services/ass-text.ts`. That decoder mirrors mpv's `ass_to_plaintext` so a cue read from a file reads identically to the same line arriving live on `sub-text`: `{...}` override blocks are markup, `\pN … \p0` vector drawing runs are dropped rather than shown as text, `\N`/`\n`/`\h` are the only escapes (`\{`, `\}` and `\\` are not), and an unclosed `{` is rendered verbatim. Every layer downstream — renderer, timing tracker, tokenizer, tokenization cache keys — receives plain text and uses `normalizePlainSubtitleText()` for whitespace only, so nothing decodes the same string twice and one authored line always maps to one cache key.
|
||||
|
||||
**Duplicate collapsing.** Typeset scripts emit one `Dialogue:` event per animation frame, plus layered copies of the same line. The parser collapses identical text over an identical span unconditionally, and collapses contiguous same-text runs of at least three events when the run looks like an animation. For ASS that means shared style and actor plus authoring evidence: a temporal tag (`\t`, `\move`, `\k`/`\kf`/`\ko`/`\K`, or anything wrapped in `\t(...)`), an animated `Effect` column (`Karaoke`, `Banner`, `Scroll`), or override values that change across the run. Static tags shared by every event (`\pos`, an identical `\clip`) are not evidence. SRT/VTT carry no such metadata, so there collapsing needs at least five contiguous events all under 0.1s — the frame timing left behind by ASS-to-SRT conversion. The parser keeps this authoring metadata (style, actor, layer, `Effect`, parsed override commands, source order) private; `parseSubtitleCues()` returns only `SubtitleCue`.
|
||||
|
||||
ASS scripts can also redraw one complete lyric for two or more long color/highlight phases. Those flush-timed phases collapse separately from short animation frames when they share text, style, actor, and layer and carry direct animation evidence, such as temporal tags or changing non-spatial overrides. Spatial command changes do not prove a phase, so separately positioned signs remain distinct.
|
||||
|
||||
**Canonical animation recovery.** Some ASS producers keep the readable lyric or sign as a timed `Comment:` and generate hundreds of `Dialogue:` frames containing repeated glyphs or changing clip regions. Others retain the complete line as brief `Dialogue:` events around the generated fragments. A complete event is promoted only when nearby dialogue from the same style and actor forms a proven animation cluster and reconstructs its entire text in source order. The generated frames are then replaced by one cue marked `source: 'canonical-ass'`. This source marker lets the live primary-subtitle path prefer the clean authored text and timing for display, sidebar history, immersion recording, and mining, while unmatched editor notes and alternative translations remain ignored. Secondary selection advances to an entering canonical cue at its generated animation start when the preceding authored cue ends before the new authored span. Unrelated simultaneous cues that continue through the new span remain visible.
|
||||
|
||||
**Font texture cleanup.** A clipped repeated-glyph run or frequent changes to secondary alpha marks a texture seed. Clipped runs do not need a font override because some signs build their masks from ordinary `l` glyphs. The parser removes short clipped pieces that share a no-font seed's style and timing, or pieces that share a font seed's style, timing, and font even when the actor changes. It also removes positioned text layers with at least `E0` global alpha when they overlap a seed in the same style. Opaque authored sign text stays publishable when the texture switches fonts or actors around it.
|
||||
|
||||
#### Prefetch Service Lifecycle
|
||||
|
||||
1. **Activation trigger:** When a subtitle track is activated (or changes), check if it's external via MPV's `track-list` property. If `external === true`, read the file via `external-filename` using the existing `loadSubtitleSourceText` infrastructure.
|
||||
2. **Parse phase:** Parse all cues from the file content. Sort by start time. Store as an ordered array.
|
||||
3. **Priority window:** Determine the current playback position. Identify the next 10 cues as the priority window.
|
||||
4. **Priority tokenization:** Tokenize the priority window cues sequentially, storing results into the `SubtitleProcessingController`'s tokenization cache.
|
||||
5. **Background tokenization:** After the priority window is done, tokenize remaining cues working forward from the current position, then wrapping around to cover earlier cues. The prefetcher stops once it has tokenized all cues or the cache is full (whichever comes first) to avoid wasteful eviction churn. For files with more cues than the cache limit, background tokenization focuses on cues ahead of the current position.
|
||||
6. **Seek handling:** On seek, re-compute the priority window from the new position. A seek is detected by observing MPV's `time-pos` property and checking if the delta from the last observed position exceeds a threshold (e.g., > 3 seconds forward or any backward jump). The current in-flight tokenization finishes naturally, then the new priority window takes over.
|
||||
7. **Teardown:** When the subtitle track changes or playback ends, stop all prefetch work and discard state.
|
||||
|
||||
#### Live Priority
|
||||
|
||||
The prefetcher and live subtitle handler share the Yomitan parser (single-threaded IPC). Live subtitle requests must always take priority. The prefetcher:
|
||||
|
||||
- Checks a `paused` flag before each cue tokenization. The live handler sets `paused = true` on subtitle change and clears it after emission.
|
||||
- Yields between each background cue tokenization (via `setTimeout(0)` or equivalent) so the live handler can set the pause flag between cues.
|
||||
- When paused, the prefetcher waits (polling the flag on a short interval or awaiting a resume signal) before continuing with the next cue.
|
||||
|
||||
#### Cache Integration
|
||||
|
||||
The prefetcher calls the same `tokenizeSubtitle` function used by live processing to produce `SubtitleData` results, then stores them into the existing `SubtitleProcessingController` tokenization cache via a new method:
|
||||
|
||||
```typescript
|
||||
// New methods on SubtitleProcessingController
|
||||
preCacheTokenization: (text: string, data: SubtitleData) => void;
|
||||
isCacheFull: () => boolean;
|
||||
```
|
||||
|
||||
`preCacheTokenization` uses the same `setCachedTokenization` logic internally (LRU eviction, Map-based storage). `isCacheFull` returns `true` when the cache has reached its limit, allowing the prefetcher to stop background tokenization and avoid wasteful eviction churn.
|
||||
|
||||
#### Cache Invalidation
|
||||
|
||||
When the user marks a word as known (or any event triggers `invalidateTokenizationCache()`), all cached results are cleared -- including prefetched ones, since they share the same cache. After invalidation, the prefetcher re-computes the priority window from the current playback position and re-tokenizes those cues to restore warm cache state.
|
||||
|
||||
#### Error Handling
|
||||
|
||||
If the subtitle file is malformed or partially parseable, the cue parser uses what it can extract. A file that yields zero cues disables prefetching silently (falls back to live-only processing). Encoding errors from `loadSubtitleSourceText` are caught and logged; prefetching is skipped for that track.
|
||||
|
||||
#### Integration Points
|
||||
|
||||
- **MPV property subscriptions:** Needs `track-list` (to detect external subtitle file path) and `time-pos` (to track playback position for window calculation and seek detection).
|
||||
- **File loading:** Uses existing `loadSubtitleSourceText` dependency.
|
||||
- **Tokenization:** Calls the same `tokenizeSubtitle` function used by live processing.
|
||||
- **Cache:** Writes into `SubtitleProcessingController`'s cache.
|
||||
- **Cache invalidation:** Listens for cache invalidation events to re-prefetch the priority window.
|
||||
|
||||
### Files Affected
|
||||
|
||||
- **New:** `src/core/services/subtitle-prefetch.ts` -- the prefetch service
|
||||
- **New:** `src/core/services/subtitle-cue-parser.ts` -- SRT/VTT/ASS cue parser (text + timing)
|
||||
- **Modified:** `src/core/services/subtitle-processing-controller.ts` -- expose `preCacheTokenization` method
|
||||
- **Modified:** `src/main.ts` -- wire up the prefetch service, listen to track changes
|
||||
|
||||
---
|
||||
|
||||
## Optimization 2: Batched Annotation Pass
|
||||
|
||||
### Summary
|
||||
|
||||
Collapse the 4 sequential annotation passes (`applyKnownWordMarking` -> `applyFrequencyMarking` -> `applyJlptMarking` -> `markNPlusOneTargets`) into a single iteration over the token array, followed by N+1 marking.
|
||||
|
||||
**Important context:** Frequency rank _values_ (`token.frequencyRank`) are already assigned at the parser level by `applyFrequencyRanks()` in `tokenizer.ts`, before the annotation stage is called. The annotation stage's `applyFrequencyMarking` only performs POS-based _filtering_ -- clearing `frequencyRank` to `undefined` for tokens that should be excluded (particles, noise tokens, etc.) and normalizing valid ranks. This optimization does not change the parser-level frequency rank assignment; it only batches the annotation-level filtering.
|
||||
|
||||
### Current Flow (4 passes, 4 array copies)
|
||||
|
||||
```text
|
||||
tokens (already have frequencyRank values from parser-level applyFrequencyRanks)
|
||||
-> applyKnownWordMarking() // .map() -> new array
|
||||
-> applyFrequencyMarking() // .map() -> new array (POS-based filtering only)
|
||||
-> applyJlptMarking() // .map() -> new array
|
||||
-> markNPlusOneTargets() // .map() -> new array
|
||||
```
|
||||
|
||||
### Dependency Analysis
|
||||
|
||||
All annotations either depend on MeCab POS data or benefit from running after it:
|
||||
|
||||
- **Known word marking:** Needs base tokens (surface/headword). No POS dependency, but no reason to run separately.
|
||||
- **Frequency filtering:** Uses `pos1Exclusions` and `pos2Exclusions` to clear frequency ranks on excluded tokens (particles, noise). Depends on MeCab POS data.
|
||||
- **JLPT marking:** Uses `shouldIgnoreJlptForMecabPos1` to filter. Depends on MeCab POS data.
|
||||
- **N+1 marking:** Uses POS exclusion sets to filter candidates. Depends on known word status + MeCab POS.
|
||||
|
||||
Since frequency filtering and JLPT marking both depend on POS data from MeCab enrichment, and MeCab enrichment already happens before the annotation stage, all four can run in a single pass after MeCab completes.
|
||||
|
||||
### New Flow (1 pass + N+1)
|
||||
|
||||
```typescript
|
||||
function annotateTokens(tokens, deps, options): MergedToken[] {
|
||||
const pos1Exclusions = resolvePos1Exclusions(options);
|
||||
const pos2Exclusions = resolvePos2Exclusions(options);
|
||||
|
||||
// Single pass: known word + frequency filtering + JLPT computed together
|
||||
const annotated = tokens.map((token) => {
|
||||
const isKnown = nPlusOneEnabled ? token.isKnown || computeIsKnown(token, deps) : false;
|
||||
|
||||
// Filter frequency rank using POS exclusions (rank values already set at parser level)
|
||||
const frequencyRank = frequencyEnabled
|
||||
? filterFrequencyRank(token, pos1Exclusions, pos2Exclusions)
|
||||
: undefined;
|
||||
|
||||
const jlptLevel = jlptEnabled ? computeJlptLevel(token, deps.getJlptLevel) : undefined;
|
||||
|
||||
return { ...token, isKnown, frequencyRank, jlptLevel };
|
||||
});
|
||||
|
||||
// N+1 must run after known word status is set for all tokens
|
||||
if (nPlusOneEnabled) {
|
||||
return markNPlusOneTargets(annotated, minSentenceWords, pos1Exclusions, pos2Exclusions);
|
||||
}
|
||||
|
||||
return annotated;
|
||||
}
|
||||
```
|
||||
|
||||
### What Changes
|
||||
|
||||
- The individual `applyKnownWordMarking`, `applyFrequencyMarking`, `applyJlptMarking` functions are refactored into per-token computation helpers (pure functions that compute a single field). The frequency helper is named `filterFrequencyRank` to clarify it performs POS-based exclusion, not rank computation.
|
||||
- The `annotateTokens` orchestrator runs one `.map()` call that invokes all three helpers per token.
|
||||
- `markNPlusOneTargets` remains a separate pass because it needs the full array with `isKnown` set (it examines sentence-level context).
|
||||
- The parser-level `applyFrequencyRanks()` call in `tokenizer.ts` is unchanged -- it remains a separate step outside the annotation stage.
|
||||
- Net: 4 array copies + 4 iterations become 1 array copy + 1 iteration + N+1 pass.
|
||||
|
||||
### Expected Savings
|
||||
|
||||
~15-45ms saved (3 fewer array allocations + 3 fewer full iterations). Annotation drops from ~25-70ms to ~10-25ms.
|
||||
|
||||
### Files Affected
|
||||
|
||||
- **Modified:** `src/core/services/tokenizer/annotation-stage.ts` -- refactor into batched single-pass
|
||||
|
||||
---
|
||||
|
||||
## Optimization 3: DOM Template Pooling
|
||||
|
||||
### Summary
|
||||
|
||||
Replace `document.createElement('span')` calls in the renderer with `templateSpan.cloneNode(false)` from a pre-created template element.
|
||||
|
||||
### Current Behavior
|
||||
|
||||
In `renderWithTokens` (`subtitle-render.ts`), each render cycle:
|
||||
|
||||
1. Clears DOM with `innerHTML = ''`
|
||||
2. Creates a `DocumentFragment`
|
||||
3. Calls `document.createElement('span')` for each token (~10-15 per subtitle)
|
||||
4. Sets `className`, `textContent`, `dataset.*` individually
|
||||
5. Appends fragment to root
|
||||
|
||||
### New Behavior
|
||||
|
||||
1. At renderer initialization (`createSubtitleRenderer`), create a single template:
|
||||
```typescript
|
||||
const templateSpan = document.createElement('span');
|
||||
```
|
||||
2. In `renderWithTokens`, replace every `document.createElement('span')` with:
|
||||
```typescript
|
||||
const span = templateSpan.cloneNode(false) as HTMLSpanElement;
|
||||
```
|
||||
3. Replace all `innerHTML = ''` calls with `root.replaceChildren()` to avoid the HTML parser invocation on clear. This applies to `renderSubtitle` (primary subtitle root), `renderSecondarySub` (secondary subtitle root), and `renderCharacterLevel` if applicable.
|
||||
4. Everything else stays the same (setting className, textContent, dataset, appending to fragment).
|
||||
|
||||
### Why cloneNode Over Full Node Recycling
|
||||
|
||||
Full recycling (collecting old nodes, clearing attributes, reusing them) requires carefully resetting every `dataset.*` property that might have been set on a previous render. This is error-prone -- a stale `data-frequency-rank` from a previous subtitle appearing on a new token would cause incorrect styling. `cloneNode(false)` on a bare template is nearly as fast and produces a clean node every time.
|
||||
|
||||
### Expected Savings
|
||||
|
||||
`cloneNode(false)` is ~2-3x faster than `createElement` in most browser engines. For 10-15 tokens per subtitle: ~3-8ms saved per render cycle.
|
||||
|
||||
### Files Affected
|
||||
|
||||
- **Modified:** `src/renderer/subtitle-render.ts` -- template creation + cloneNode usage
|
||||
|
||||
---
|
||||
|
||||
## Combined Impact Summary
|
||||
|
||||
| Scenario | Before | After | Improvement |
|
||||
| --------------------------------- | ---------- | ---------- | ----------- |
|
||||
| Normal playback (prefetch-warmed) | ~200-320ms | ~30-50ms | ~80-85% |
|
||||
| Cache hit (repeated subtitle) | ~72ms | ~55-65ms | ~10-20% |
|
||||
| Cache miss (immediate seek) | ~200-320ms | ~150-260ms | ~20-25% |
|
||||
|
||||
---
|
||||
|
||||
## Files Summary
|
||||
|
||||
### New Files
|
||||
|
||||
- `src/core/services/subtitle-prefetch.ts`
|
||||
- `src/core/services/subtitle-cue-parser.ts`
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `src/core/services/subtitle-processing-controller.ts` (expose `preCacheTokenization`)
|
||||
- `src/core/services/tokenizer/annotation-stage.ts` (batched single-pass)
|
||||
- `src/renderer/subtitle-render.ts` (template cloneNode)
|
||||
- `src/main.ts` (wire up prefetch service)
|
||||
|
||||
### Test Files
|
||||
|
||||
- New tests for subtitle cue parser (SRT, VTT, ASS formats)
|
||||
- New tests for subtitle prefetch service (priority window, seek, pause/resume)
|
||||
- Updated tests for annotation stage (same behavior, new implementation)
|
||||
- Updated tests for subtitle render (template cloning)
|
||||
@@ -6,11 +6,9 @@ import type { LauncherCommandContext } from './context.js';
|
||||
import { runConfigCommand } from './config-command.js';
|
||||
import { runDictionaryCommand } from './dictionary-command.js';
|
||||
import { runDoctorCommand } from './doctor-command.js';
|
||||
import { runLogsCommand } from './logs-command.js';
|
||||
import { runMpvPostAppCommand, runMpvPreAppCommand } from './mpv-command.js';
|
||||
import { runAppPassthroughCommand } from './app-command.js';
|
||||
import { runStatsCommand } from './stats-command.js';
|
||||
import { runUpdateCommand } from './update-command.js';
|
||||
|
||||
class ExitSignal extends Error {
|
||||
code: number;
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function runMpvPostAppCommand(
|
||||
context: LauncherCommandContext,
|
||||
deps: MpvCommandDeps = defaultDeps,
|
||||
): Promise<boolean> {
|
||||
const { args, appPath, scriptPath, mpvSocketPath, pluginRuntimeConfig } = context;
|
||||
const { args, appPath, mpvSocketPath, pluginRuntimeConfig } = context;
|
||||
if (!args.mpvIdle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,20 +17,11 @@ test('parseLauncherYoutubeSubgenConfig keeps only valid typed values', () => {
|
||||
secondarySub: {
|
||||
secondarySubLanguages: ['eng', true, 'deu'],
|
||||
},
|
||||
jimaku: {
|
||||
apiKey: 'abc',
|
||||
apiKeyCommand: 'pass show key',
|
||||
apiBaseUrl: 'https://jimaku.cc',
|
||||
languagePreference: 'ja',
|
||||
maxEntryResults: 8.7,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal('mode' in parsed, false);
|
||||
assert.deepEqual(parsed.primarySubLanguages, ['ja', 'en']);
|
||||
assert.deepEqual(parsed.secondarySubLanguages, ['eng', 'deu']);
|
||||
assert.equal(parsed.jimakuLanguagePreference, 'ja');
|
||||
assert.equal(parsed.jimakuMaxEntryResults, 8);
|
||||
});
|
||||
|
||||
test('parseLauncherJellyfinConfig omits legacy token and user id fields', () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
LogLevel,
|
||||
} from '../types.js';
|
||||
import {
|
||||
DEFAULT_JIMAKU_API_BASE_URL,
|
||||
DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS,
|
||||
DEFAULT_YOUTUBE_SECONDARY_SUB_LANGS,
|
||||
} from '../types.js';
|
||||
@@ -125,11 +124,6 @@ export function createDefaultArgs(
|
||||
recursive: false,
|
||||
profile: mpvConfig.profile ?? '',
|
||||
startOverlay: false,
|
||||
jimakuApiKey: process.env.SUBMINER_JIMAKU_API_KEY || '',
|
||||
jimakuApiKeyCommand: process.env.SUBMINER_JIMAKU_API_KEY_COMMAND || '',
|
||||
jimakuApiBaseUrl: process.env.SUBMINER_JIMAKU_API_BASE_URL || DEFAULT_JIMAKU_API_BASE_URL,
|
||||
jimakuLanguagePreference: launcherConfig.jimakuLanguagePreference || 'ja',
|
||||
jimakuMaxEntryResults: launcherConfig.jimakuMaxEntryResults || 10,
|
||||
jellyfin: false,
|
||||
jellyfinLogin: false,
|
||||
jellyfinLogout: false,
|
||||
@@ -184,15 +178,6 @@ export function createDefaultArgs(
|
||||
targetKind: '',
|
||||
};
|
||||
|
||||
if (launcherConfig.jimakuApiKey) parsed.jimakuApiKey = launcherConfig.jimakuApiKey;
|
||||
if (launcherConfig.jimakuApiKeyCommand)
|
||||
parsed.jimakuApiKeyCommand = launcherConfig.jimakuApiKeyCommand;
|
||||
if (launcherConfig.jimakuApiBaseUrl) parsed.jimakuApiBaseUrl = launcherConfig.jimakuApiBaseUrl;
|
||||
if (launcherConfig.jimakuLanguagePreference)
|
||||
parsed.jimakuLanguagePreference = launcherConfig.jimakuLanguagePreference;
|
||||
if (launcherConfig.jimakuMaxEntryResults !== undefined)
|
||||
parsed.jimakuMaxEntryResults = launcherConfig.jimakuMaxEntryResults;
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,31 +16,9 @@ export function parseLauncherYoutubeSubgenConfig(
|
||||
secondarySubRaw && typeof secondarySubRaw === 'object'
|
||||
? (secondarySubRaw as Record<string, unknown>)
|
||||
: null;
|
||||
const jimakuRaw = root.jimaku;
|
||||
const jimaku =
|
||||
jimakuRaw && typeof jimakuRaw === 'object' ? (jimakuRaw as Record<string, unknown>) : null;
|
||||
|
||||
const jimakuLanguagePreference = jimaku?.languagePreference;
|
||||
const jimakuMaxEntryResults = jimaku?.maxEntryResults;
|
||||
|
||||
return {
|
||||
primarySubLanguages: asStringArray(youtube?.primarySubLanguages),
|
||||
secondarySubLanguages: asStringArray(secondarySub?.secondarySubLanguages),
|
||||
jimakuApiKey: typeof jimaku?.apiKey === 'string' ? jimaku.apiKey : undefined,
|
||||
jimakuApiKeyCommand:
|
||||
typeof jimaku?.apiKeyCommand === 'string' ? jimaku.apiKeyCommand : undefined,
|
||||
jimakuApiBaseUrl: typeof jimaku?.apiBaseUrl === 'string' ? jimaku.apiBaseUrl : undefined,
|
||||
jimakuLanguagePreference:
|
||||
jimakuLanguagePreference === 'ja' ||
|
||||
jimakuLanguagePreference === 'en' ||
|
||||
jimakuLanguagePreference === 'none'
|
||||
? jimakuLanguagePreference
|
||||
: undefined,
|
||||
jimakuMaxEntryResults:
|
||||
typeof jimakuMaxEntryResults === 'number' &&
|
||||
Number.isFinite(jimakuMaxEntryResults) &&
|
||||
jimakuMaxEntryResults > 0
|
||||
? Math.floor(jimakuMaxEntryResults)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import type { Args, JimakuLanguagePreference } from './types.js';
|
||||
import { DEFAULT_JIMAKU_API_BASE_URL } from './types.js';
|
||||
import { commandExists } from './util.js';
|
||||
|
||||
export interface JimakuEntry {
|
||||
id: number;
|
||||
name: string;
|
||||
english_name?: string | null;
|
||||
japanese_name?: string | null;
|
||||
flags?: {
|
||||
anime?: boolean;
|
||||
movie?: boolean;
|
||||
adult?: boolean;
|
||||
external?: boolean;
|
||||
unverified?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface JimakuFileEntry {
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
last_modified: string;
|
||||
}
|
||||
|
||||
interface JimakuApiError {
|
||||
error: string;
|
||||
code?: number;
|
||||
retryAfter?: number;
|
||||
}
|
||||
|
||||
type JimakuApiResponse<T> = { ok: true; data: T } | { ok: false; error: JimakuApiError };
|
||||
|
||||
type JimakuDownloadResult = { ok: true; path: string } | { ok: false; error: JimakuApiError };
|
||||
|
||||
interface JimakuConfig {
|
||||
apiKey: string;
|
||||
apiKeyCommand: string;
|
||||
apiBaseUrl: string;
|
||||
languagePreference: JimakuLanguagePreference;
|
||||
maxEntryResults: number;
|
||||
}
|
||||
|
||||
interface JimakuMediaInfo {
|
||||
title: string;
|
||||
season: number | null;
|
||||
episode: number | null;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
filename: string;
|
||||
rawTitle: string;
|
||||
}
|
||||
|
||||
function getRetryAfter(headers: http.IncomingHttpHeaders): number | undefined {
|
||||
const value = headers['x-ratelimit-reset-after'];
|
||||
if (!value) return undefined;
|
||||
const raw = Array.isArray(value) ? value[0] : value;
|
||||
if (!raw) return undefined;
|
||||
const parsed = Number.parseFloat(raw);
|
||||
if (!Number.isFinite(parsed)) return undefined;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function matchEpisodeFromName(name: string): {
|
||||
season: number | null;
|
||||
episode: number | null;
|
||||
index: number | null;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
} {
|
||||
const seasonEpisode = name.match(/S(\d{1,2})E(\d{1,3})/i);
|
||||
if (seasonEpisode && seasonEpisode.index !== undefined) {
|
||||
const seasonText = seasonEpisode[1];
|
||||
const episodeText = seasonEpisode[2];
|
||||
if (!seasonText || !episodeText) {
|
||||
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||
}
|
||||
return {
|
||||
season: Number.parseInt(seasonText, 10),
|
||||
episode: Number.parseInt(episodeText, 10),
|
||||
index: seasonEpisode.index,
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
const alt = name.match(/(\d{1,2})x(\d{1,3})/i);
|
||||
if (alt && alt.index !== undefined) {
|
||||
const seasonText = alt[1];
|
||||
const episodeText = alt[2];
|
||||
if (!seasonText || !episodeText) {
|
||||
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||
}
|
||||
return {
|
||||
season: Number.parseInt(seasonText, 10),
|
||||
episode: Number.parseInt(episodeText, 10),
|
||||
index: alt.index,
|
||||
confidence: 'high',
|
||||
};
|
||||
}
|
||||
|
||||
const epOnly = name.match(/(?:^|[\s._-])E(?:P)?(\d{1,3})(?:\b|[\s._-])/i);
|
||||
if (epOnly && epOnly.index !== undefined) {
|
||||
const episodeText = epOnly[1];
|
||||
if (!episodeText) {
|
||||
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||
}
|
||||
return {
|
||||
season: null,
|
||||
episode: Number.parseInt(episodeText, 10),
|
||||
index: epOnly.index,
|
||||
confidence: 'medium',
|
||||
};
|
||||
}
|
||||
|
||||
const numeric = name.match(/(?:^|[-–—]\s*)(\d{1,3})\s*[-–—]/);
|
||||
if (numeric && numeric.index !== undefined) {
|
||||
const episodeText = numeric[1];
|
||||
if (!episodeText) {
|
||||
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||
}
|
||||
return {
|
||||
season: null,
|
||||
episode: Number.parseInt(episodeText, 10),
|
||||
index: numeric.index,
|
||||
confidence: 'medium',
|
||||
};
|
||||
}
|
||||
|
||||
return { season: null, episode: null, index: null, confidence: 'low' };
|
||||
}
|
||||
|
||||
function detectSeasonFromDir(mediaPath: string): number | null {
|
||||
const parent = path.basename(path.dirname(mediaPath));
|
||||
const match = parent.match(/(?:Season|S)\s*(\d{1,2})/i);
|
||||
if (!match) return null;
|
||||
const seasonText = match[1];
|
||||
if (!seasonText) return null;
|
||||
const parsed = Number.parseInt(seasonText, 10);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseGuessitOutput(mediaPath: string, stdout: string): JimakuMediaInfo | null {
|
||||
const payload = stdout.trim();
|
||||
if (!payload) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(payload) as {
|
||||
title?: string;
|
||||
title_original?: string;
|
||||
series?: string;
|
||||
season?: number | string;
|
||||
episode?: number | string;
|
||||
episode_list?: Array<number | string>;
|
||||
};
|
||||
const season =
|
||||
typeof parsed.season === 'number'
|
||||
? parsed.season
|
||||
: typeof parsed.season === 'string'
|
||||
? Number.parseInt(parsed.season, 10)
|
||||
: null;
|
||||
const directEpisode =
|
||||
typeof parsed.episode === 'number'
|
||||
? parsed.episode
|
||||
: typeof parsed.episode === 'string'
|
||||
? Number.parseInt(parsed.episode, 10)
|
||||
: null;
|
||||
const episodeFromList =
|
||||
parsed.episode_list && parsed.episode_list.length > 0
|
||||
? Number.parseInt(String(parsed.episode_list[0]), 10)
|
||||
: null;
|
||||
const episodeValue =
|
||||
directEpisode !== null && Number.isFinite(directEpisode) ? directEpisode : episodeFromList;
|
||||
const episode = Number.isFinite(episodeValue as number) ? (episodeValue as number) : null;
|
||||
const title = (parsed.title || parsed.title_original || parsed.series || '').trim();
|
||||
const hasStructuredData =
|
||||
title.length > 0 ||
|
||||
Number.isFinite(season as number) ||
|
||||
Number.isFinite(episodeValue as number);
|
||||
|
||||
if (!hasStructuredData) return null;
|
||||
|
||||
return {
|
||||
title: title || '',
|
||||
season: Number.isFinite(season as number) ? season : detectSeasonFromDir(mediaPath),
|
||||
episode: episode,
|
||||
confidence: 'high',
|
||||
filename: path.basename(mediaPath),
|
||||
rawTitle: path.basename(mediaPath).replace(/\.[^/.]+$/, ''),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseMediaInfoWithGuessit(mediaPath: string): JimakuMediaInfo | null {
|
||||
if (!commandExists('guessit')) return null;
|
||||
|
||||
try {
|
||||
const fileName = path.basename(mediaPath);
|
||||
const result = spawnSync('guessit', ['--json', fileName], {
|
||||
cwd: path.dirname(mediaPath),
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 2_000_000,
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.error || result.status !== 0) return null;
|
||||
return parseGuessitOutput(mediaPath, result.stdout || '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupTitle(value: string): string {
|
||||
return value
|
||||
.replace(/^[\s-–—]+/, '')
|
||||
.replace(/[\s-–—]+$/, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function formatLangScore(name: string, pref: JimakuLanguagePreference): number {
|
||||
if (pref === 'none') return 0;
|
||||
const upper = name.toUpperCase();
|
||||
const hasJa =
|
||||
/(^|[\W_])JA([\W_]|$)/.test(upper) ||
|
||||
/(^|[\W_])JPN([\W_]|$)/.test(upper) ||
|
||||
upper.includes('.JA.');
|
||||
const hasEn =
|
||||
/(^|[\W_])EN([\W_]|$)/.test(upper) ||
|
||||
/(^|[\W_])ENG([\W_]|$)/.test(upper) ||
|
||||
upper.includes('.EN.');
|
||||
if (pref === 'ja') {
|
||||
if (hasJa) return 2;
|
||||
if (hasEn) return 1;
|
||||
} else if (pref === 'en') {
|
||||
if (hasEn) return 2;
|
||||
if (hasJa) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function resolveJimakuApiKey(config: JimakuConfig): Promise<string | null> {
|
||||
if (config.apiKey && config.apiKey.trim()) {
|
||||
return config.apiKey.trim();
|
||||
}
|
||||
if (config.apiKeyCommand && config.apiKeyCommand.trim()) {
|
||||
try {
|
||||
const commandResult = spawnSync(config.apiKeyCommand, {
|
||||
shell: true,
|
||||
encoding: 'utf8',
|
||||
timeout: 10000,
|
||||
});
|
||||
if (commandResult.error) return null;
|
||||
const key = (commandResult.stdout || '').trim();
|
||||
return key.length > 0 ? key : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function jimakuFetchJson<T>(
|
||||
endpoint: string,
|
||||
query: Record<string, string | number | boolean | null | undefined>,
|
||||
options: { baseUrl: string; apiKey: string },
|
||||
): Promise<JimakuApiResponse<T>> {
|
||||
const url = new URL(endpoint, options.baseUrl);
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value === null || value === undefined) continue;
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const requestUrl = new URL(url.toString());
|
||||
const transport = requestUrl.protocol === 'https:' ? https : http;
|
||||
const req = transport.request(
|
||||
requestUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: options.apiKey,
|
||||
'User-Agent': 'SubMiner',
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
res.on('end', () => {
|
||||
const status = res.statusCode || 0;
|
||||
if (status >= 200 && status < 300) {
|
||||
try {
|
||||
const parsed = JSON.parse(data) as T;
|
||||
resolve({ ok: true, data: parsed });
|
||||
} catch {
|
||||
resolve({
|
||||
ok: false,
|
||||
error: { error: 'Failed to parse Jimaku response JSON.' },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let errorMessage = `Jimaku API error (HTTP ${status})`;
|
||||
try {
|
||||
const parsed = JSON.parse(data) as { error?: string };
|
||||
if (parsed && parsed.error) {
|
||||
errorMessage = parsed.error;
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
|
||||
resolve({
|
||||
ok: false,
|
||||
error: {
|
||||
error: errorMessage,
|
||||
code: status || undefined,
|
||||
retryAfter: status === 429 ? getRetryAfter(res.headers) : undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on('error', (error) => {
|
||||
resolve({
|
||||
ok: false,
|
||||
error: { error: `Jimaku request failed: ${(error as Error).message}` },
|
||||
});
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function parseMediaInfo(mediaPath: string | null): JimakuMediaInfo {
|
||||
if (!mediaPath) {
|
||||
return {
|
||||
title: '',
|
||||
season: null,
|
||||
episode: null,
|
||||
confidence: 'low',
|
||||
filename: '',
|
||||
rawTitle: '',
|
||||
};
|
||||
}
|
||||
|
||||
const guessitInfo = parseMediaInfoWithGuessit(mediaPath);
|
||||
if (guessitInfo) return guessitInfo;
|
||||
|
||||
const filename = path.basename(mediaPath);
|
||||
let name = filename.replace(/\.[^/.]+$/, '');
|
||||
name = name.replace(/\[[^\]]*]/g, ' ');
|
||||
name = name.replace(/\(\d{4}\)/g, ' ');
|
||||
name = name.replace(/[._]/g, ' ');
|
||||
name = name.replace(/[–—]/g, '-');
|
||||
name = name.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const parsed = matchEpisodeFromName(name);
|
||||
let titlePart = name;
|
||||
if (parsed.index !== null) {
|
||||
titlePart = name.slice(0, parsed.index);
|
||||
}
|
||||
|
||||
const seasonFromDir = parsed.season ?? detectSeasonFromDir(mediaPath);
|
||||
const title = cleanupTitle(titlePart || name);
|
||||
|
||||
return {
|
||||
title,
|
||||
season: seasonFromDir,
|
||||
episode: parsed.episode,
|
||||
confidence: parsed.confidence,
|
||||
filename,
|
||||
rawTitle: name,
|
||||
};
|
||||
}
|
||||
|
||||
export function sortJimakuFiles(
|
||||
files: JimakuFileEntry[],
|
||||
pref: JimakuLanguagePreference,
|
||||
): JimakuFileEntry[] {
|
||||
if (pref === 'none') return files;
|
||||
return [...files].sort((a, b) => {
|
||||
const scoreDiff = formatLangScore(b.name, pref) - formatLangScore(a.name, pref);
|
||||
if (scoreDiff !== 0) return scoreDiff;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadToFile(
|
||||
url: string,
|
||||
destPath: string,
|
||||
headers: Record<string, string>,
|
||||
redirectCount = 0,
|
||||
): Promise<JimakuDownloadResult> {
|
||||
if (redirectCount > 3) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { error: 'Too many redirects while downloading subtitle.' },
|
||||
};
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const parsedUrl = new URL(url);
|
||||
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
||||
|
||||
const req = transport.get(parsedUrl, { headers }, (res) => {
|
||||
const status = res.statusCode || 0;
|
||||
if ([301, 302, 303, 307, 308].includes(status) && res.headers.location) {
|
||||
const redirectUrl = new URL(res.headers.location, parsedUrl).toString();
|
||||
res.resume();
|
||||
downloadToFile(redirectUrl, destPath, headers, redirectCount + 1).then(resolve);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
res.resume();
|
||||
resolve({
|
||||
ok: false,
|
||||
error: {
|
||||
error: `Failed to download subtitle (HTTP ${status}).`,
|
||||
code: status,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fileStream = fs.createWriteStream(destPath);
|
||||
res.pipe(fileStream);
|
||||
fileStream.on('finish', () => {
|
||||
fileStream.close(() => {
|
||||
resolve({ ok: true, path: destPath });
|
||||
});
|
||||
});
|
||||
fileStream.on('error', (err: Error) => {
|
||||
resolve({
|
||||
ok: false,
|
||||
error: { error: `Failed to save subtitle: ${err.message}` },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
resolve({
|
||||
ok: false,
|
||||
error: {
|
||||
error: `Download request failed: ${(err as Error).message}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function isValidSubtitleCandidateFile(filename: string): boolean {
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
return ext === '.srt' || ext === '.vtt' || ext === '.ass' || ext === '.ssa' || ext === '.sub';
|
||||
}
|
||||
|
||||
export function mapPreferenceToLanguages(preference: JimakuLanguagePreference): string[] {
|
||||
if (preference === 'en') return ['en', 'eng'];
|
||||
if (preference === 'none') return [];
|
||||
return ['ja', 'jpn'];
|
||||
}
|
||||
|
||||
export function normalizeJimakuSearchInput(mediaPath: string): string {
|
||||
const trimmed = (mediaPath || '').trim();
|
||||
if (!trimmed) return '';
|
||||
if (!/^https?:\/\/.*/.test(trimmed)) return trimmed;
|
||||
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
const titleParam =
|
||||
url.searchParams.get('title') || url.searchParams.get('name') || url.searchParams.get('q');
|
||||
if (titleParam && titleParam.trim()) return titleParam.trim();
|
||||
|
||||
const pathParts = url.pathname.split('/').filter(Boolean).reverse();
|
||||
const candidate = pathParts.find((part) => {
|
||||
const decoded = decodeURIComponent(part || '').replace(/\.[^/.]+$/, '');
|
||||
const lowered = decoded.toLowerCase();
|
||||
return lowered.length > 2 && !/^[0-9.]+$/.test(lowered) && !/^[a-f0-9]{16,}$/i.test(lowered);
|
||||
});
|
||||
|
||||
const fallback = candidate || url.hostname.replace(/^www\./, '');
|
||||
return sanitizeJimakuQueryInput(decodeURIComponent(fallback));
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeJimakuQueryInput(value: string): string {
|
||||
return value
|
||||
.replace(/^\s*-\s*/, '')
|
||||
.replace(/[^\w\s\-'".:(),]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function buildJimakuConfig(args: Args): {
|
||||
apiKey: string;
|
||||
apiKeyCommand: string;
|
||||
apiBaseUrl: string;
|
||||
languagePreference: JimakuLanguagePreference;
|
||||
maxEntryResults: number;
|
||||
} {
|
||||
return {
|
||||
apiKey: args.jimakuApiKey,
|
||||
apiKeyCommand: args.jimakuApiKeyCommand,
|
||||
apiBaseUrl: args.jimakuApiBaseUrl || DEFAULT_JIMAKU_API_BASE_URL,
|
||||
languagePreference: args.jimakuLanguagePreference,
|
||||
maxEntryResults: args.jimakuMaxEntryResults || 10,
|
||||
};
|
||||
}
|
||||
@@ -543,7 +543,6 @@ test(
|
||||
const appStartEntries = readJsonLines(appStartPath);
|
||||
const appStopEntries = readJsonLines(appStopPath);
|
||||
const controlEntries = readJsonLines(controlServer.logPath);
|
||||
const mpvEntries = readJsonLines(path.join(smokeCase.artifactsDir, 'fake-mpv.log'));
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
if (process.platform === 'linux') {
|
||||
|
||||
@@ -52,7 +52,6 @@ export function getDefaultMpvLogFile(options?: {
|
||||
|
||||
export const DEFAULT_MPV_LOG_FILE = getDefaultMpvLogFile();
|
||||
export const DEFAULT_YOUTUBE_YTDL_FORMAT = 'bestvideo*+bestaudio/best';
|
||||
export const DEFAULT_JIMAKU_API_BASE_URL = 'https://jimaku.cc';
|
||||
export const DEFAULT_MPV_SUBMINER_ARGS = [
|
||||
'--sub-auto=fuzzy',
|
||||
'--sub-file-paths=.;subs;subtitles',
|
||||
@@ -69,7 +68,6 @@ export function shouldForwardLogLevel(level: LogLevel): boolean {
|
||||
return level === 'debug' || level === 'error';
|
||||
}
|
||||
export type Backend = 'auto' | 'hyprland' | 'sway' | 'x11' | 'macos' | 'windows';
|
||||
export type JimakuLanguagePreference = 'ja' | 'en' | 'none';
|
||||
|
||||
export interface Args {
|
||||
generateSubtitles?: {
|
||||
@@ -103,11 +101,6 @@ export interface Args {
|
||||
passwordStore: string;
|
||||
target: string;
|
||||
targetKind: '' | 'file' | 'url';
|
||||
jimakuApiKey: string;
|
||||
jimakuApiKeyCommand: string;
|
||||
jimakuApiBaseUrl: string;
|
||||
jimakuLanguagePreference: JimakuLanguagePreference;
|
||||
jimakuMaxEntryResults: number;
|
||||
jellyfin: boolean;
|
||||
jellyfinLogin: boolean;
|
||||
jellyfinLogout: boolean;
|
||||
@@ -151,11 +144,6 @@ export interface Args {
|
||||
export interface LauncherYoutubeSubgenConfig {
|
||||
primarySubLanguages?: string[];
|
||||
secondarySubLanguages?: string[];
|
||||
jimakuApiKey?: string;
|
||||
jimakuApiKeyCommand?: string;
|
||||
jimakuApiBaseUrl?: string;
|
||||
jimakuLanguagePreference?: JimakuLanguagePreference;
|
||||
jimakuMaxEntryResults?: number;
|
||||
}
|
||||
|
||||
export interface LauncherJellyfinConfig {
|
||||
|
||||
@@ -114,8 +114,6 @@
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@fontsource-variable/geist-mono": "^5.2.7",
|
||||
"@xhayper/discord-rpc": "^1.3.4",
|
||||
"axios": "^1.20.0",
|
||||
"commander": "^14.0.3",
|
||||
@@ -133,7 +131,6 @@
|
||||
"electron": "43.7.7",
|
||||
"electron-builder": "26.16.1",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^10.8.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"undici": "7.29.1"
|
||||
|
||||
@@ -56,9 +56,6 @@ function M.init()
|
||||
ctx.binary = make_lazy_proxy("binary", function()
|
||||
return require("binary").create(ctx)
|
||||
end)
|
||||
ctx.hover = make_lazy_proxy("hover", function()
|
||||
return require("hover").create(ctx)
|
||||
end)
|
||||
ctx.process = make_lazy_proxy("process", function()
|
||||
return require("process").create(ctx)
|
||||
end)
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
local M = {}
|
||||
|
||||
local DEFAULT_HOVER_BASE_COLOR = "FFFFFF"
|
||||
local DEFAULT_HOVER_COLOR = "C6A0F6"
|
||||
|
||||
function M.create(ctx)
|
||||
local mp = ctx.mp
|
||||
local msg = ctx.msg
|
||||
local utils = ctx.utils
|
||||
local state = ctx.state
|
||||
|
||||
local function to_hex_color(input)
|
||||
if type(input) ~= "string" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local hex = input:gsub("[%#%']", ""):gsub("^0x", "")
|
||||
if #hex ~= 6 and #hex ~= 3 then
|
||||
return nil
|
||||
end
|
||||
if #hex == 3 then
|
||||
return hex:sub(1, 1) .. hex:sub(1, 1) .. hex:sub(2, 2) .. hex:sub(2, 2) .. hex:sub(3, 3) .. hex:sub(3, 3)
|
||||
end
|
||||
return hex
|
||||
end
|
||||
|
||||
local function fix_ass_color(input, fallback)
|
||||
local hex = to_hex_color(input)
|
||||
if not hex then
|
||||
return fallback or DEFAULT_HOVER_BASE_COLOR
|
||||
end
|
||||
local r, g, b = hex:sub(1, 2), hex:sub(3, 4), hex:sub(5, 6)
|
||||
return b .. g .. r
|
||||
end
|
||||
|
||||
local function sanitize_hover_ass_color(input, fallback_rgb)
|
||||
local fallback = fix_ass_color(fallback_rgb or DEFAULT_HOVER_COLOR, DEFAULT_HOVER_COLOR)
|
||||
local converted = fix_ass_color(input, fallback)
|
||||
if converted == "000000" then
|
||||
return fallback
|
||||
end
|
||||
return converted
|
||||
end
|
||||
|
||||
local function escape_ass_text(text)
|
||||
return (text or ""):gsub("\\", "\\\\"):gsub("{", "\\{"):gsub("}", "\\}"):gsub("\n", "\\N")
|
||||
end
|
||||
|
||||
local function resolve_osd_dimensions()
|
||||
local width = mp.get_property_number("osd-width", 0) or 0
|
||||
local height = mp.get_property_number("osd-height", 0) or 0
|
||||
|
||||
if width <= 0 or height <= 0 then
|
||||
local osd_dims = mp.get_property_native("osd-dimensions")
|
||||
if type(osd_dims) == "table" and type(osd_dims.w) == "number" and osd_dims.w > 0 then
|
||||
width = osd_dims.w
|
||||
end
|
||||
if type(osd_dims) == "table" and type(osd_dims.h) == "number" and osd_dims.h > 0 then
|
||||
height = osd_dims.h
|
||||
end
|
||||
end
|
||||
|
||||
if width <= 0 then
|
||||
width = 1280
|
||||
end
|
||||
if height <= 0 then
|
||||
height = 720
|
||||
end
|
||||
|
||||
return width, height
|
||||
end
|
||||
|
||||
local function resolve_metrics()
|
||||
local sub_font_size = mp.get_property_number("sub-font-size", 36) or 36
|
||||
local sub_scale = mp.get_property_number("sub-scale", 1) or 1
|
||||
local sub_scale_by_window = mp.get_property_bool("sub-scale-by-window", true) == true
|
||||
local sub_pos = mp.get_property_number("sub-pos", 100) or 100
|
||||
local sub_margin_y = mp.get_property_number("sub-margin-y", 0) or 0
|
||||
local sub_font = mp.get_property("sub-font", "sans-serif") or "sans-serif"
|
||||
local sub_spacing = mp.get_property_number("sub-spacing", 0) or 0
|
||||
local sub_bold = mp.get_property_bool("sub-bold", false) == true
|
||||
local sub_italic = mp.get_property_bool("sub-italic", false) == true
|
||||
local sub_border_size = mp.get_property_number("sub-border-size", 2) or 2
|
||||
local sub_shadow_offset = mp.get_property_number("sub-shadow-offset", 0) or 0
|
||||
local osd_w, osd_h = resolve_osd_dimensions()
|
||||
local window_scale = 1
|
||||
if sub_scale_by_window and osd_h > 0 then
|
||||
window_scale = osd_h / 720
|
||||
end
|
||||
local effective_margin_y = sub_margin_y * window_scale
|
||||
|
||||
return {
|
||||
font_size = sub_font_size * (sub_scale > 0 and sub_scale or 1) * window_scale,
|
||||
pos = sub_pos,
|
||||
margin_y = effective_margin_y,
|
||||
font = sub_font,
|
||||
spacing = sub_spacing,
|
||||
bold = sub_bold,
|
||||
italic = sub_italic,
|
||||
border = sub_border_size * window_scale,
|
||||
shadow = sub_shadow_offset * window_scale,
|
||||
base_color = fix_ass_color(mp.get_property("sub-color"), DEFAULT_HOVER_BASE_COLOR),
|
||||
hover_color = sanitize_hover_ass_color(nil, DEFAULT_HOVER_COLOR),
|
||||
}
|
||||
end
|
||||
|
||||
local function get_subtitle_ass_property()
|
||||
local ass_text = mp.get_property("sub-text/ass")
|
||||
if ass_text ~= nil then
|
||||
if type(ass_text) == "string" and ass_text ~= "" then
|
||||
return ass_text
|
||||
end
|
||||
return nil
|
||||
end
|
||||
ass_text = mp.get_property("sub-text-ass")
|
||||
if type(ass_text) == "string" and ass_text ~= "" then
|
||||
return ass_text
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function plain_text_and_ass_map(text)
|
||||
local plain = {}
|
||||
local map = {}
|
||||
local plain_len = 0
|
||||
local i = 1
|
||||
local text_len = #text
|
||||
|
||||
while i <= text_len do
|
||||
local ch = text:sub(i, i)
|
||||
if ch == "{" then
|
||||
local close = text:find("}", i + 1, true)
|
||||
if not close then
|
||||
break
|
||||
end
|
||||
i = close + 1
|
||||
elseif ch == "\\" then
|
||||
local esc = text:sub(i + 1, i + 1)
|
||||
if esc == "N" or esc == "n" then
|
||||
plain_len = plain_len + 1
|
||||
plain[plain_len] = "\n"
|
||||
map[plain_len] = i
|
||||
i = i + 2
|
||||
elseif esc == "h" then
|
||||
plain_len = plain_len + 1
|
||||
plain[plain_len] = " "
|
||||
map[plain_len] = i
|
||||
i = i + 2
|
||||
elseif esc == "{" then
|
||||
plain_len = plain_len + 1
|
||||
plain[plain_len] = "{"
|
||||
map[plain_len] = i
|
||||
i = i + 2
|
||||
elseif esc == "}" then
|
||||
plain_len = plain_len + 1
|
||||
plain[plain_len] = "}"
|
||||
map[plain_len] = i
|
||||
i = i + 2
|
||||
elseif esc == "\\" then
|
||||
plain_len = plain_len + 1
|
||||
plain[plain_len] = "\\"
|
||||
map[plain_len] = i
|
||||
i = i + 2
|
||||
else
|
||||
local seq_end = i + 1
|
||||
while seq_end <= text_len and text:sub(seq_end, seq_end):match("[%a]") do
|
||||
seq_end = seq_end + 1
|
||||
end
|
||||
if text:sub(seq_end, seq_end) == "(" then
|
||||
local close = text:find(")", seq_end, true)
|
||||
if close then
|
||||
i = close + 1
|
||||
else
|
||||
i = seq_end + 1
|
||||
end
|
||||
else
|
||||
i = seq_end + 1
|
||||
end
|
||||
end
|
||||
else
|
||||
plain_len = plain_len + 1
|
||||
plain[plain_len] = ch
|
||||
map[plain_len] = i
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(plain), map
|
||||
end
|
||||
|
||||
local function find_hover_span(payload, plain)
|
||||
local source_len = #plain
|
||||
local cursor = 1
|
||||
for _, token in ipairs(payload.tokens or {}) do
|
||||
if type(token) == "table" and type(token.text) == "string" and token.text ~= "" then
|
||||
local token_text = token.text
|
||||
local start_pos = nil
|
||||
local end_pos = nil
|
||||
|
||||
if type(token.startPos) == "number" and type(token.endPos) == "number" then
|
||||
if token.startPos >= 0 and token.endPos >= token.startPos then
|
||||
local candidate_start = token.startPos + 1
|
||||
local candidate_stop = token.endPos
|
||||
if candidate_start >= 1 and candidate_stop <= source_len and candidate_stop >= candidate_start and plain:sub(candidate_start, candidate_stop) == token_text then
|
||||
start_pos = candidate_start
|
||||
end_pos = candidate_stop
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not start_pos or not end_pos then
|
||||
local fallback_start, fallback_stop = plain:find(token_text, cursor, true)
|
||||
if not fallback_start then
|
||||
fallback_start, fallback_stop = plain:find(token_text, 1, true)
|
||||
end
|
||||
start_pos, end_pos = fallback_start, fallback_stop
|
||||
end
|
||||
|
||||
if start_pos and end_pos then
|
||||
if token.index == payload.hoveredTokenIndex then
|
||||
return start_pos, end_pos
|
||||
end
|
||||
cursor = end_pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function inject_hover_color_to_ass(raw_ass, plain_map, hover_start, hover_end, hover_color, base_color)
|
||||
if hover_start == nil or hover_end == nil then
|
||||
return raw_ass
|
||||
end
|
||||
|
||||
local raw_open_idx = plain_map[hover_start] or 1
|
||||
local raw_close_idx = plain_map[hover_end + 1] or (#raw_ass + 1)
|
||||
if raw_open_idx < 1 then
|
||||
raw_open_idx = 1
|
||||
end
|
||||
if raw_close_idx < 1 then
|
||||
raw_close_idx = 1
|
||||
end
|
||||
if raw_open_idx > #raw_ass + 1 then
|
||||
raw_open_idx = #raw_ass + 1
|
||||
end
|
||||
if raw_close_idx > #raw_ass + 1 then
|
||||
raw_close_idx = #raw_ass + 1
|
||||
end
|
||||
|
||||
local before = raw_ass:sub(1, raw_open_idx - 1)
|
||||
local hovered = raw_ass:sub(raw_open_idx, raw_close_idx - 1)
|
||||
local after = raw_ass:sub(raw_close_idx)
|
||||
local hover_suffix = string.format("\\1c&H%s&", hover_color)
|
||||
|
||||
-- Keep hover foreground stable even when inline ASS override tags (\1c/\c/\r) appear inside token.
|
||||
hovered = hovered:gsub("{([^}]*)}", function(inner)
|
||||
if inner:find("\\1c&H", 1, true) or inner:find("\\c&H", 1, true) or inner:find("\\r", 1, true) then
|
||||
return "{" .. inner .. hover_suffix .. "}"
|
||||
end
|
||||
return "{" .. inner .. "}"
|
||||
end)
|
||||
|
||||
local open_tag = string.format("{\\1c&H%s&}", hover_color)
|
||||
local close_tag = string.format("{\\1c&H%s&}", base_color)
|
||||
return before .. open_tag .. hovered .. close_tag .. after
|
||||
end
|
||||
|
||||
local function build_hover_subtitle_content(payload)
|
||||
local source_ass = get_subtitle_ass_property()
|
||||
if type(source_ass) == "string" and source_ass ~= "" then
|
||||
state.hover_highlight.cached_ass = source_ass
|
||||
else
|
||||
source_ass = state.hover_highlight.cached_ass
|
||||
end
|
||||
if type(source_ass) ~= "string" or source_ass == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local plain_source, plain_map = plain_text_and_ass_map(source_ass)
|
||||
if type(plain_source) ~= "string" or plain_source == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local hover_start, hover_end = find_hover_span(payload, plain_source)
|
||||
if not hover_start or not hover_end then
|
||||
return nil
|
||||
end
|
||||
|
||||
local metrics = resolve_metrics()
|
||||
local hover_color = sanitize_hover_ass_color(payload.colors and payload.colors.hover or nil, DEFAULT_HOVER_COLOR)
|
||||
local base_color = fix_ass_color(payload.colors and payload.colors.base or nil, metrics.base_color)
|
||||
return inject_hover_color_to_ass(source_ass, plain_map, hover_start, hover_end, hover_color, base_color)
|
||||
end
|
||||
|
||||
local function clear_hover_overlay()
|
||||
if state.hover_highlight.clear_timer then
|
||||
state.hover_highlight.clear_timer:kill()
|
||||
state.hover_highlight.clear_timer = nil
|
||||
end
|
||||
if state.hover_highlight.overlay_active then
|
||||
if type(state.hover_highlight.saved_sub_visibility) == "string" then
|
||||
mp.set_property("sub-visibility", state.hover_highlight.saved_sub_visibility)
|
||||
else
|
||||
mp.set_property("sub-visibility", "yes")
|
||||
end
|
||||
if type(state.hover_highlight.saved_secondary_sub_visibility) == "string" then
|
||||
mp.set_property("secondary-sub-visibility", state.hover_highlight.saved_secondary_sub_visibility)
|
||||
end
|
||||
state.hover_highlight.saved_sub_visibility = nil
|
||||
state.hover_highlight.saved_secondary_sub_visibility = nil
|
||||
state.hover_highlight.overlay_active = false
|
||||
end
|
||||
mp.set_osd_ass(0, 0, "")
|
||||
state.hover_highlight.payload = nil
|
||||
state.hover_highlight.revision = -1
|
||||
state.hover_highlight.cached_ass = nil
|
||||
state.hover_highlight.last_hover_update_ts = 0
|
||||
end
|
||||
|
||||
local function schedule_hover_clear(delay_seconds)
|
||||
if state.hover_highlight.clear_timer then
|
||||
state.hover_highlight.clear_timer:kill()
|
||||
state.hover_highlight.clear_timer = nil
|
||||
end
|
||||
state.hover_highlight.clear_timer = mp.add_timeout(delay_seconds or 0.08, function()
|
||||
state.hover_highlight.clear_timer = nil
|
||||
clear_hover_overlay()
|
||||
end)
|
||||
end
|
||||
|
||||
local function render_hover_overlay(payload)
|
||||
if not payload or payload.hoveredTokenIndex == nil or payload.subtitle == nil then
|
||||
clear_hover_overlay()
|
||||
return
|
||||
end
|
||||
|
||||
local ass = build_hover_subtitle_content(payload)
|
||||
if not ass then
|
||||
return
|
||||
end
|
||||
|
||||
local osd_w, osd_h = resolve_osd_dimensions()
|
||||
local metrics = resolve_metrics()
|
||||
local osd_dims = mp.get_property_native("osd-dimensions")
|
||||
local ml = (type(osd_dims) == "table" and type(osd_dims.ml) == "number") and osd_dims.ml or 0
|
||||
local mr = (type(osd_dims) == "table" and type(osd_dims.mr) == "number") and osd_dims.mr or 0
|
||||
local mt = (type(osd_dims) == "table" and type(osd_dims.mt) == "number") and osd_dims.mt or 0
|
||||
local mb = (type(osd_dims) == "table" and type(osd_dims.mb) == "number") and osd_dims.mb or 0
|
||||
local usable_w = math.max(1, osd_w - ml - mr)
|
||||
local usable_h = math.max(1, osd_h - mt - mb)
|
||||
local anchor_x = math.floor(ml + usable_w / 2)
|
||||
local baseline_adjust = (metrics.border + metrics.shadow) * 5
|
||||
local anchor_y = math.floor(mt + (usable_h * metrics.pos / 100) - metrics.margin_y + baseline_adjust)
|
||||
local font_size = math.max(8, metrics.font_size)
|
||||
local anchor_tag = string.format(
|
||||
"{\\an2\\q2\\pos(%d,%d)\\fn%s\\fs%g\\b%d\\i%d\\fsp%g\\bord%g\\shad%g\\1c&H%s&}",
|
||||
anchor_x,
|
||||
anchor_y,
|
||||
escape_ass_text(metrics.font),
|
||||
font_size,
|
||||
metrics.bold and 1 or 0,
|
||||
metrics.italic and 1 or 0,
|
||||
metrics.spacing,
|
||||
metrics.border,
|
||||
metrics.shadow,
|
||||
metrics.base_color
|
||||
)
|
||||
if not state.hover_highlight.overlay_active then
|
||||
state.hover_highlight.saved_sub_visibility = mp.get_property("sub-visibility")
|
||||
state.hover_highlight.saved_secondary_sub_visibility = mp.get_property("secondary-sub-visibility")
|
||||
mp.set_property("sub-visibility", "no")
|
||||
mp.set_property("secondary-sub-visibility", "no")
|
||||
state.hover_highlight.overlay_active = true
|
||||
end
|
||||
mp.set_osd_ass(osd_w, osd_h, anchor_tag .. ass)
|
||||
end
|
||||
|
||||
local function handle_hover_message(payload_json)
|
||||
local parsed, parse_error = utils.parse_json(payload_json)
|
||||
if not parsed then
|
||||
msg.warn("Invalid hover-highlight payload: " .. tostring(parse_error))
|
||||
clear_hover_overlay()
|
||||
return
|
||||
end
|
||||
|
||||
if type(parsed.revision) ~= "number" then
|
||||
clear_hover_overlay()
|
||||
return
|
||||
end
|
||||
|
||||
if parsed.revision < state.hover_highlight.revision then
|
||||
return
|
||||
end
|
||||
|
||||
if type(parsed.hoveredTokenIndex) == "number" and type(parsed.tokens) == "table" then
|
||||
if state.hover_highlight.clear_timer then
|
||||
state.hover_highlight.clear_timer:kill()
|
||||
state.hover_highlight.clear_timer = nil
|
||||
end
|
||||
state.hover_highlight.revision = parsed.revision
|
||||
state.hover_highlight.payload = parsed
|
||||
state.hover_highlight.last_hover_update_ts = mp.get_time() or 0
|
||||
render_hover_overlay(state.hover_highlight.payload)
|
||||
return
|
||||
end
|
||||
|
||||
local now = mp.get_time() or 0
|
||||
local elapsed_since_hover = now - (state.hover_highlight.last_hover_update_ts or 0)
|
||||
state.hover_highlight.revision = parsed.revision
|
||||
state.hover_highlight.payload = nil
|
||||
if state.hover_highlight.overlay_active then
|
||||
if elapsed_since_hover > 0.35 then
|
||||
return
|
||||
end
|
||||
schedule_hover_clear(0.08)
|
||||
else
|
||||
clear_hover_overlay()
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
HOVER_MESSAGE_NAME = "subminer-hover-token",
|
||||
HOVER_MESSAGE_NAME_LEGACY = "yomipv-hover-token",
|
||||
handle_hover_message = handle_hover_message,
|
||||
clear_hover_overlay = clear_hover_overlay,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -10,7 +10,6 @@ function M.create(ctx)
|
||||
local state = ctx.state
|
||||
local options_helper = ctx.options_helper
|
||||
local process = ctx.process
|
||||
local hover = ctx.hover
|
||||
local subminer_log = ctx.log.subminer_log
|
||||
local show_osd = ctx.log.show_osd
|
||||
|
||||
@@ -308,7 +307,6 @@ function M.create(ctx)
|
||||
|
||||
local function on_shutdown()
|
||||
next_auto_start_retry_generation()
|
||||
hover.clear_hover_overlay()
|
||||
process.disarm_auto_play_ready_gate()
|
||||
process.stop_overlay_loading_osd()
|
||||
clear_pending_visible_overlay_hide()
|
||||
@@ -326,13 +324,9 @@ function M.create(ctx)
|
||||
mp.register_event("start-file", on_start_file)
|
||||
mp.register_event("file-loaded", on_file_loaded)
|
||||
mp.register_event("shutdown", on_shutdown)
|
||||
mp.register_event("file-loaded", function()
|
||||
hover.clear_hover_overlay()
|
||||
end)
|
||||
mp.register_event("end-file", function(event)
|
||||
process.stop_overlay_loading_osd()
|
||||
process.disarm_auto_play_ready_gate()
|
||||
hover.clear_hover_overlay()
|
||||
local reason = type(event) == "table" and event.reason or nil
|
||||
if is_reload_end_file(reason) then
|
||||
state.pending_reload_media_identity = state.current_media_identity or resolve_media_identity()
|
||||
@@ -352,15 +346,6 @@ function M.create(ctx)
|
||||
hide_visible_overlay_after_end_file()
|
||||
end
|
||||
end)
|
||||
mp.register_event("shutdown", function()
|
||||
hover.clear_hover_overlay()
|
||||
end)
|
||||
mp.add_hook("on_unload", 10, function()
|
||||
hover.clear_hover_overlay()
|
||||
end)
|
||||
mp.observe_property("sub-start", "native", function()
|
||||
hover.clear_hover_overlay()
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,6 @@ function M.create(ctx)
|
||||
local mp = ctx.mp
|
||||
local opts = ctx.opts
|
||||
local process = ctx.process
|
||||
local hover = ctx.hover
|
||||
local ui = ctx.ui
|
||||
local state = ctx.state
|
||||
|
||||
@@ -49,12 +48,6 @@ function M.create(ctx)
|
||||
mp.register_script_message("subminer-overlay-loading-ready", function()
|
||||
process.stop_overlay_loading_osd()
|
||||
end)
|
||||
mp.register_script_message(hover.HOVER_MESSAGE_NAME, function(payload_json)
|
||||
hover.handle_hover_message(payload_json)
|
||||
end)
|
||||
mp.register_script_message(hover.HOVER_MESSAGE_NAME_LEGACY, function(payload_json)
|
||||
hover.handle_hover_message(payload_json)
|
||||
end)
|
||||
mp.register_script_message("subminer-stats-toggle", function()
|
||||
if opts.osd_messages then
|
||||
mp.osd_message("Stats: press ` (backtick) in overlay", 3)
|
||||
|
||||
@@ -8,16 +8,6 @@ function M.new()
|
||||
binary_available = false,
|
||||
binary_path = nil,
|
||||
detected_backend = nil,
|
||||
hover_highlight = {
|
||||
revision = -1,
|
||||
payload = nil,
|
||||
saved_sub_visibility = nil,
|
||||
saved_secondary_sub_visibility = nil,
|
||||
overlay_active = false,
|
||||
cached_ass = nil,
|
||||
clear_timer = nil,
|
||||
last_hover_update_ts = 0,
|
||||
},
|
||||
auto_play_ready_gate_armed = false,
|
||||
auto_play_ready_should_resume_playback = false,
|
||||
auto_play_ready_timeout = nil,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
local MODULE_PATHS = {
|
||||
"plugin/subminer/bootstrap.lua",
|
||||
"plugin/subminer/hover.lua",
|
||||
"plugin/subminer/environment.lua",
|
||||
"plugin/subminer/version.lua",
|
||||
}
|
||||
|
||||
@@ -267,7 +267,6 @@ local function run_plugin_scenario(config)
|
||||
package.loaded["binary"] = nil
|
||||
package.loaded["bootstrap"] = nil
|
||||
package.loaded["environment"] = nil
|
||||
package.loaded["hover"] = nil
|
||||
package.loaded["init"] = nil
|
||||
package.loaded["lifecycle"] = nil
|
||||
package.loaded["log"] = nil
|
||||
|
||||
@@ -41,7 +41,6 @@ import type { NPlusOneMatchMode, SubtitleMiningContext } from './types/subtitle'
|
||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from './config';
|
||||
import {
|
||||
getConfiguredWordFieldCandidates,
|
||||
getConfiguredWordFieldName,
|
||||
getPreferredWordValueFromExtractedFields,
|
||||
} from './anki-field-config';
|
||||
import { createLogger } from './logger';
|
||||
@@ -1632,10 +1631,6 @@ export class AnkiIntegration {
|
||||
return sentenceCardConfig.audioField || 'SentenceAudio';
|
||||
}
|
||||
|
||||
private getConfiguredWordFieldName(): string {
|
||||
return getConfiguredWordFieldName(this.config);
|
||||
}
|
||||
|
||||
private getConfiguredWordFieldCandidates(): string[] {
|
||||
return getConfiguredWordFieldCandidates(this.config);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
|
||||
import type { AnkiConnectConfig } from '../types/anki';
|
||||
import {
|
||||
getKnownWordCacheLifecycleConfig,
|
||||
getKnownWordCacheRefreshIntervalMinutes,
|
||||
getKnownWordCacheScopeForConfig,
|
||||
} from './known-word-cache';
|
||||
import { getKnownWordCacheLifecycleConfig } from './known-word-cache';
|
||||
|
||||
export interface AnkiIntegrationRuntimeProxyServer {
|
||||
start(options: { host: string; port: number; upstreamUrl: string }): void;
|
||||
@@ -255,14 +251,6 @@ export class AnkiIntegrationRuntime {
|
||||
return getKnownWordCacheLifecycleConfig(config);
|
||||
}
|
||||
|
||||
private getKnownWordRefreshIntervalMinutes(config: AnkiConnectConfig): number {
|
||||
return getKnownWordCacheRefreshIntervalMinutes(config);
|
||||
}
|
||||
|
||||
private getKnownWordCacheScopeForConfig(config: AnkiConnectConfig): string {
|
||||
return getKnownWordCacheScopeForConfig(config);
|
||||
}
|
||||
|
||||
getOrCreateProxyServer(): AnkiIntegrationRuntimeProxyServer {
|
||||
if (!this.proxyServer) {
|
||||
this.proxyServer = this.deps.proxyServerFactory();
|
||||
|
||||
@@ -133,9 +133,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
enabled: false,
|
||||
accessToken: '',
|
||||
characterDictionary: {
|
||||
refreshTtlHours: 168,
|
||||
maxLoaded: 3,
|
||||
evictionPolicy: 'delete',
|
||||
profileScope: 'all',
|
||||
collapsibleSections: {
|
||||
description: false,
|
||||
|
||||
@@ -37,8 +37,6 @@ function collectConfigLeafPaths(config: ResolvedConfig): string[] {
|
||||
// an allowlist entry. Only allowlist a path when the registry is genuinely
|
||||
// the wrong surface for it.
|
||||
const UNDOCUMENTED_LEAVES: ReadonlySet<string> = new Set([
|
||||
'anilist.characterDictionary.evictionPolicy',
|
||||
'anilist.characterDictionary.refreshTtlHours',
|
||||
'keybindings',
|
||||
'subtitleStyle.backdropFilter',
|
||||
'subtitleStyle.backgroundColor',
|
||||
|
||||
@@ -94,27 +94,6 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
||||
if (isObject(src.anilist.characterDictionary)) {
|
||||
const characterDictionary = src.anilist.characterDictionary;
|
||||
|
||||
const refreshTtlHours = asNumber(characterDictionary.refreshTtlHours);
|
||||
if (refreshTtlHours !== undefined) {
|
||||
const normalized = Math.min(24 * 365, Math.max(1, Math.floor(refreshTtlHours)));
|
||||
if (normalized !== refreshTtlHours) {
|
||||
warn(
|
||||
'anilist.characterDictionary.refreshTtlHours',
|
||||
characterDictionary.refreshTtlHours,
|
||||
normalized,
|
||||
'Out of range; clamped to 1..8760 hours.',
|
||||
);
|
||||
}
|
||||
resolved.anilist.characterDictionary.refreshTtlHours = normalized;
|
||||
} else if (characterDictionary.refreshTtlHours !== undefined) {
|
||||
warn(
|
||||
'anilist.characterDictionary.refreshTtlHours',
|
||||
characterDictionary.refreshTtlHours,
|
||||
resolved.anilist.characterDictionary.refreshTtlHours,
|
||||
'Expected number.',
|
||||
);
|
||||
}
|
||||
|
||||
const maxLoaded = asNumber(characterDictionary.maxLoaded);
|
||||
if (maxLoaded !== undefined) {
|
||||
const normalized = Math.min(20, Math.max(1, Math.floor(maxLoaded)));
|
||||
@@ -136,28 +115,6 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
const evictionPolicyRaw = asString(characterDictionary.evictionPolicy);
|
||||
if (evictionPolicyRaw !== undefined) {
|
||||
const evictionPolicy = evictionPolicyRaw.trim().toLowerCase();
|
||||
if (evictionPolicy === 'disable' || evictionPolicy === 'delete') {
|
||||
resolved.anilist.characterDictionary.evictionPolicy = evictionPolicy;
|
||||
} else {
|
||||
warn(
|
||||
'anilist.characterDictionary.evictionPolicy',
|
||||
characterDictionary.evictionPolicy,
|
||||
resolved.anilist.characterDictionary.evictionPolicy,
|
||||
"Expected one of: 'disable', 'delete'.",
|
||||
);
|
||||
}
|
||||
} else if (characterDictionary.evictionPolicy !== undefined) {
|
||||
warn(
|
||||
'anilist.characterDictionary.evictionPolicy',
|
||||
characterDictionary.evictionPolicy,
|
||||
resolved.anilist.characterDictionary.evictionPolicy,
|
||||
'Expected string.',
|
||||
);
|
||||
}
|
||||
|
||||
const profileScopeRaw = asString(characterDictionary.profileScope);
|
||||
if (profileScopeRaw !== undefined) {
|
||||
const profileScope = profileScopeRaw.trim().toLowerCase();
|
||||
|
||||
@@ -97,9 +97,7 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
|
||||
const { context, warnings } = createResolveContext({
|
||||
anilist: {
|
||||
characterDictionary: {
|
||||
refreshTtlHours: 0,
|
||||
maxLoaded: 99,
|
||||
evictionPolicy: 'purge' as never,
|
||||
profileScope: 'global' as never,
|
||||
collapsibleSections: {
|
||||
description: true,
|
||||
@@ -112,9 +110,7 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
|
||||
|
||||
applyIntegrationConfig(context);
|
||||
|
||||
assert.equal(context.resolved.anilist.characterDictionary.refreshTtlHours, 1);
|
||||
assert.equal(context.resolved.anilist.characterDictionary.maxLoaded, 20);
|
||||
assert.equal(context.resolved.anilist.characterDictionary.evictionPolicy, 'delete');
|
||||
assert.equal(context.resolved.anilist.characterDictionary.profileScope, 'all');
|
||||
assert.equal(context.resolved.anilist.characterDictionary.collapsibleSections.description, true);
|
||||
assert.equal(
|
||||
@@ -124,9 +120,7 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
|
||||
assert.equal(context.resolved.anilist.characterDictionary.collapsibleSections.voicedBy, true);
|
||||
|
||||
const warnedPaths = warnings.map((warning) => warning.path);
|
||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.refreshTtlHours'));
|
||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.maxLoaded'));
|
||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.evictionPolicy'));
|
||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.profileScope'));
|
||||
assert.ok(
|
||||
warnedPaths.includes('anilist.characterDictionary.collapsibleSections.characterInformation'),
|
||||
@@ -134,7 +128,7 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
|
||||
});
|
||||
|
||||
test('yomitan externalProfilePath is trimmed and invalid values warn', () => {
|
||||
const { context, warnings } = createResolveContext({
|
||||
const { context } = createResolveContext({
|
||||
yomitan: {
|
||||
externalProfilePath: ' /tmp/gsm-profile ',
|
||||
},
|
||||
|
||||
@@ -18,10 +18,6 @@ const TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY = new Map(
|
||||
CONFIG_TEMPLATE_SECTIONS.map((section) => [String(section.key), section.description[0] ?? '']),
|
||||
);
|
||||
const SUBTITLE_CSS_SCOPES: SubtitleCssScope[] = ['primary', 'secondary', 'sidebar'];
|
||||
const HIDDEN_TEMPLATE_PATHS = [
|
||||
'anilist.characterDictionary.evictionPolicy',
|
||||
'anilist.characterDictionary.refreshTtlHours',
|
||||
];
|
||||
|
||||
function normalizeCommentText(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').replace(/\*\//g, '*\\/').trim();
|
||||
@@ -176,9 +172,6 @@ function renderSection(
|
||||
function createTemplateConfig(config: ResolvedConfig): ResolvedConfig {
|
||||
const templateConfig = deepCloneConfig(config);
|
||||
foldSubtitleCssManagedDefaults(templateConfig);
|
||||
for (const hiddenPath of HIDDEN_TEMPLATE_PATHS) {
|
||||
deleteValueAtPath(templateConfig, hiddenPath);
|
||||
}
|
||||
if (templateConfig.keybindings.length === 0) {
|
||||
templateConfig.keybindings = DEFAULT_KEYBINDINGS.map((binding) => ({
|
||||
key: binding.key,
|
||||
|
||||
@@ -87,20 +87,6 @@ export interface GuessAnilistMediaInfoDeps {
|
||||
runGuessit: (target: string) => Promise<string>;
|
||||
}
|
||||
|
||||
function firstString(value: unknown): string | null {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
const candidate = firstString(item);
|
||||
if (candidate) return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeGuessitTitlePart(value: string): string {
|
||||
return value.replace(/[._]+/g, ' ').replace(/-/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
@@ -147,16 +147,10 @@ import { enqueueWrite } from './immersion-tracker/queue';
|
||||
import { nowMs } from './immersion-tracker/time';
|
||||
import {
|
||||
DEFAULT_BATCH_SIZE,
|
||||
DEFAULT_DAILY_ROLLUP_RETENTION_MS,
|
||||
DEFAULT_EVENTS_RETENTION_MS,
|
||||
DEFAULT_FLUSH_INTERVAL_MS,
|
||||
DEFAULT_MAINTENANCE_INTERVAL_MS,
|
||||
DEFAULT_MAX_PAYLOAD_BYTES,
|
||||
DEFAULT_MONTHLY_ROLLUP_RETENTION_MS,
|
||||
DEFAULT_QUEUE_CAP,
|
||||
DEFAULT_SESSIONS_RETENTION_MS,
|
||||
DEFAULT_TELEMETRY_RETENTION_MS,
|
||||
DEFAULT_VACUUM_INTERVAL_MS,
|
||||
EVENT_CARD_MINED,
|
||||
EVENT_LOOKUP,
|
||||
EVENT_MEDIA_BUFFER,
|
||||
|
||||
@@ -2769,7 +2769,6 @@ test('getSessionWordsByLine joins word occurrences through imm_words.id', () =>
|
||||
|
||||
try {
|
||||
ensureSchema(db);
|
||||
const stmts = createTrackerPreparedStatements(db);
|
||||
const startedAtMs = 1_735_732_800_000;
|
||||
const videoId = getOrCreateVideoRecord(db, '/tmp/session-words-by-line.mkv', {
|
||||
canonicalTitle: 'Episode',
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
} from './types';
|
||||
import {
|
||||
ACTIVE_SESSION_METRICS_CTE,
|
||||
SESSION_WORD_COUNTS_CTE,
|
||||
SESSION_WORD_COUNTS_SELECT,
|
||||
fromDbTimestamp,
|
||||
resolvedCoverBlobExpr,
|
||||
|
||||
@@ -16,7 +16,6 @@ import { deriveStoredPartOfSpeech } from '../tokenizer/part-of-speech';
|
||||
import {
|
||||
cleanupUnusedCoverArtBlobHash,
|
||||
findSharedCoverBlobHash,
|
||||
toDbMs,
|
||||
toDbTimestamp,
|
||||
} from './query-shared';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ensureLexicalDailyRollupTables, markLexicalDailyRollupsReady } from './
|
||||
import { isVocabularyStatsRowVisible, type VocabularyVisibilityRow } from './vocabulary-visibility';
|
||||
import { SCHEMA_VERSION } from './types';
|
||||
import type { QueuedWrite, VideoMetadata, YoutubeVideoMetadata } from './types';
|
||||
import { toDbMs, toDbTimestamp } from './query-shared';
|
||||
import { toDbTimestamp } from './query-shared';
|
||||
|
||||
export interface TrackerPreparedStatements {
|
||||
telemetryInsertStmt: ReturnType<DatabaseSync['prepare']>;
|
||||
|
||||
@@ -74,9 +74,6 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
overlayInteractionActive?: boolean;
|
||||
mainWindow: BrowserWindow | null;
|
||||
windowTracker: BaseWindowTracker | null;
|
||||
lastKnownWindowsForegroundProcessName?: string | null;
|
||||
windowsOverlayProcessName?: string | null;
|
||||
windowsFocusHandoffGraceActive?: boolean;
|
||||
macOSForegroundProbeActive?: boolean;
|
||||
trackerNotReadyWarningShown: boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
@@ -168,26 +165,6 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
const shouldUseMacOSMousePassthrough = args.isMacOSPlatform && !overlayInteractionActive;
|
||||
const shouldDefaultToPassthrough =
|
||||
args.isWindowsPlatform || forceMousePassthrough || shouldReleaseMacOSOverlayLevel;
|
||||
const windowsForegroundProcessName =
|
||||
args.lastKnownWindowsForegroundProcessName?.trim().toLowerCase() ?? null;
|
||||
const windowsOverlayProcessName = args.windowsOverlayProcessName?.trim().toLowerCase() ?? null;
|
||||
const hasWindowsForegroundProcessSignal =
|
||||
args.isWindowsPlatform && windowsForegroundProcessName !== null;
|
||||
const isTrackedWindowsTargetFocused = args.windowTracker?.isTargetWindowFocused?.() ?? true;
|
||||
const isTrackedWindowsTargetMinimized =
|
||||
args.isWindowsPlatform &&
|
||||
typeof args.windowTracker?.isTargetWindowMinimized === 'function' &&
|
||||
args.windowTracker.isTargetWindowMinimized();
|
||||
const shouldPreserveWindowsOverlayDuringFocusHandoff =
|
||||
args.isWindowsPlatform &&
|
||||
args.windowsFocusHandoffGraceActive === true &&
|
||||
!!args.windowTracker &&
|
||||
(!hasWindowsForegroundProcessSignal ||
|
||||
windowsForegroundProcessName === 'mpv' ||
|
||||
(windowsOverlayProcessName !== null &&
|
||||
windowsForegroundProcessName === windowsOverlayProcessName)) &&
|
||||
!isTrackedWindowsTargetMinimized &&
|
||||
(args.windowTracker.isTracking() || args.windowTracker.getGeometry() !== null);
|
||||
const shouldForcePassiveReshow = args.isWindowsPlatform && !wasVisible;
|
||||
const isNonNativeOverlay = !args.isWindowsPlatform && !args.isMacOSPlatform;
|
||||
const isNonNativePassiveOverlay = isNonNativeOverlay && !overlayInteractionActive;
|
||||
@@ -208,13 +185,6 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
(isNonNativePassiveOverlay && !hasNonNativeInputRegion) ||
|
||||
(shouldDefaultToPassthrough && (!isVisibleOverlayFocused || shouldForcePassiveReshow));
|
||||
const shouldBindTrackedWindowsOverlay = args.isWindowsPlatform && !!args.windowTracker;
|
||||
const shouldKeepTrackedWindowsOverlayTopmost =
|
||||
!args.isWindowsPlatform ||
|
||||
!args.windowTracker ||
|
||||
isVisibleOverlayFocused ||
|
||||
isTrackedWindowsTargetFocused ||
|
||||
shouldPreserveWindowsOverlayDuringFocusHandoff ||
|
||||
(hasWindowsForegroundProcessSignal && windowsForegroundProcessName === 'mpv');
|
||||
if (shouldIgnoreMouseEvents) {
|
||||
applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { RuntimeOptionApplyResult, RuntimeOptionId } from '../../types';
|
||||
import type { SessionActionId } from '../../types/session-bindings';
|
||||
import type { SessionActionDispatchRequest } from '../../types/runtime';
|
||||
|
||||
export interface SessionActionExecutorDeps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { selectAll, type SqlRow, type SyncDb } from './libsql-driver';
|
||||
import { selectAll, type SyncDb } from './libsql-driver';
|
||||
import { nowDbTimestamp, tableExists, type SyncMergeSummary } from './shared';
|
||||
|
||||
const LOCAL_DAY_EXPR = `CAST(julianday(CAST(started_at_ms AS REAL) / 1000, 'unixepoch', 'localtime') - 2440587.5 AS INTEGER)`;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseSrtCues, parseAssCues, parseSubtitleCues } from './subtitle-cue-parser';
|
||||
import type { SubtitleCue } from '../../types';
|
||||
|
||||
test('parseSrtCues parses basic SRT content', () => {
|
||||
const content = [
|
||||
|
||||
@@ -608,14 +608,6 @@ function getYomitanFrequencyRank(
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedHeadword = token.headword.trim();
|
||||
const normalizedSurface = token.surface.trim();
|
||||
const isInflectedHeadwordFallback =
|
||||
matchMode === 'headword' &&
|
||||
normalizedCandidateText === normalizedHeadword &&
|
||||
normalizedSurface.length > 0 &&
|
||||
normalizedSurface !== normalizedHeadword;
|
||||
|
||||
return selectBestYomitanFrequencyRank(candidateEntries);
|
||||
}
|
||||
|
||||
|
||||
@@ -390,23 +390,6 @@ function isReduplicatedKanaSfx(text: string): boolean {
|
||||
return chars.slice(0, half).join('') === chars.slice(half).join('');
|
||||
}
|
||||
|
||||
function isReduplicatedKanaSfxWithOptionalTrailingTo(text: string): boolean {
|
||||
const normalized = normalizeKana(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isReduplicatedKanaSfx(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalized.length <= 1 || !normalized.endsWith('と')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isReduplicatedKanaSfx(normalized.slice(0, -1));
|
||||
}
|
||||
|
||||
function hasAdjacentKanaRepeat(text: string): boolean {
|
||||
const normalized = normalizeKana(text);
|
||||
if (!normalized) {
|
||||
|
||||
-15
@@ -193,14 +193,9 @@ import {
|
||||
createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler,
|
||||
createTrayRuntimeHandlers,
|
||||
createOverlayVisibilityRuntime,
|
||||
createBroadcastRuntimeOptionsChangedHandler,
|
||||
createGetRuntimeOptionsStateHandler,
|
||||
createGetFieldGroupingResolverHandler,
|
||||
createSetFieldGroupingResolverHandler,
|
||||
createOpenRuntimeOptionsPaletteHandler,
|
||||
createRestorePreviousSecondarySubVisibilityHandler,
|
||||
createSendToActiveOverlayWindowHandler,
|
||||
createSetOverlayDebugVisualizationEnabledHandler,
|
||||
createLoadSubtitlePositionHandler,
|
||||
createSaveSubtitlePositionHandler,
|
||||
createAppendClipboardVideoToQueueHandler,
|
||||
@@ -297,7 +292,6 @@ import {
|
||||
handleMultiCopyDigit as handleMultiCopyDigitCore,
|
||||
hasMpvWebsocketPlugin,
|
||||
importYomitanDictionaryFromZip,
|
||||
initializeOverlayAnkiIntegration as initializeOverlayAnkiIntegrationCore,
|
||||
initializeOverlayRuntime as initializeOverlayRuntimeCore,
|
||||
isSubtitleAnnotationUpgrade,
|
||||
isOverlayWindowContentReady,
|
||||
@@ -603,7 +597,6 @@ import {
|
||||
} from './main/anilist-url-guard';
|
||||
import {
|
||||
ConfigService,
|
||||
ConfigStartupParseError,
|
||||
DEFAULT_CONFIG,
|
||||
DEFAULT_KEYBINDINGS,
|
||||
generateConfigTemplate,
|
||||
@@ -615,7 +608,6 @@ import {
|
||||
createSubtitlePrefetchService,
|
||||
type SubtitlePrefetchService,
|
||||
} from './core/services/subtitle-prefetch';
|
||||
import { buildSubtitleSidebarSourceKey } from './main/runtime/subtitle-prefetch-source';
|
||||
import { createSubtitlePrefetchInitController } from './main/runtime/subtitle-prefetch-init';
|
||||
import {
|
||||
createCachedInternalSubtitleTrackExtractor,
|
||||
@@ -623,7 +615,6 @@ import {
|
||||
} from './main/runtime/internal-subtitle-extraction';
|
||||
import { createRemoteMediaPathDetector } from './main/runtime/network-media-path';
|
||||
import { applyCharacterDictionarySelection } from './main/character-dictionary-selection';
|
||||
import { getSubsyncConfig } from './subsync/utils';
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
|
||||
@@ -2798,11 +2789,6 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
||||
getOverlayInteractionActive: () =>
|
||||
visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive(),
|
||||
getWindowTracker: () => appState.windowTracker,
|
||||
getLastKnownWindowsForegroundProcessName: () =>
|
||||
visibleOverlayInteractionRuntime.getLastWindowsVisibleOverlayForegroundProcessName(),
|
||||
getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
|
||||
getWindowsFocusHandoffGraceActive: () =>
|
||||
visibleOverlayInteractionRuntime.hasWindowsVisibleOverlayFocusHandoffGrace(),
|
||||
getMacOSForegroundProbeActive: () =>
|
||||
visibleOverlayInteractionRuntime.getMacOSVisibleOverlayForegroundProbeActive(),
|
||||
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
|
||||
@@ -3233,7 +3219,6 @@ const {
|
||||
cleanupJellyfinSubtitleCache,
|
||||
runJellyfinCommand,
|
||||
openJellyfinSetupWindow,
|
||||
getJellyfinClientInfo,
|
||||
ensureMpvConnectedForPlayback,
|
||||
} = composeJellyfinRuntimeHandlers({
|
||||
getResolvedJellyfinConfigMainDeps: {
|
||||
|
||||
@@ -49,7 +49,6 @@ import type {
|
||||
CharacterDictionaryRuntimeDeps,
|
||||
CharacterDictionarySnapshot,
|
||||
CharacterDictionarySnapshotImage,
|
||||
CharacterDictionarySnapshotProgress,
|
||||
CharacterDictionarySnapshotProgressCallbacks,
|
||||
CharacterDictionarySnapshotResult,
|
||||
MergedCharacterDictionaryBuildResult,
|
||||
|
||||
@@ -15,9 +15,6 @@ export interface OverlayVisibilityRuntimeDeps {
|
||||
getSuspendVisibleOverlay?: () => boolean;
|
||||
getOverlayInteractionActive?: () => boolean;
|
||||
getWindowTracker: () => BaseWindowTracker | null;
|
||||
getLastKnownWindowsForegroundProcessName?: () => string | null;
|
||||
getWindowsOverlayProcessName?: () => string | null;
|
||||
getWindowsFocusHandoffGraceActive?: () => boolean;
|
||||
getMacOSForegroundProbeActive?: () => boolean;
|
||||
getTrackerNotReadyWarningShown: () => boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
@@ -61,9 +58,6 @@ export function createOverlayVisibilityRuntimeService(
|
||||
overlayInteractionActive: deps.getOverlayInteractionActive?.() ?? false,
|
||||
mainWindow,
|
||||
windowTracker,
|
||||
lastKnownWindowsForegroundProcessName: deps.getLastKnownWindowsForegroundProcessName?.(),
|
||||
windowsOverlayProcessName: deps.getWindowsOverlayProcessName?.() ?? null,
|
||||
windowsFocusHandoffGraceActive: deps.getWindowsFocusHandoffGraceActive?.() ?? false,
|
||||
macOSForegroundProbeActive: deps.getMacOSForegroundProbeActive?.() ?? false,
|
||||
trackerNotReadyWarningShown: deps.getTrackerNotReadyWarningShown(),
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
||||
|
||||
@@ -14,10 +14,6 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
|
||||
getSuspendVisibleOverlay: () => deps.getSuspendVisibleOverlay?.() ?? false,
|
||||
getOverlayInteractionActive: () => deps.getOverlayInteractionActive?.() ?? false,
|
||||
getWindowTracker: () => deps.getWindowTracker(),
|
||||
getLastKnownWindowsForegroundProcessName: () =>
|
||||
deps.getLastKnownWindowsForegroundProcessName?.() ?? null,
|
||||
getWindowsOverlayProcessName: () => deps.getWindowsOverlayProcessName?.() ?? null,
|
||||
getWindowsFocusHandoffGraceActive: () => deps.getWindowsFocusHandoffGraceActive?.() ?? false,
|
||||
getMacOSForegroundProbeActive: () => deps.getMacOSForegroundProbeActive?.() ?? false,
|
||||
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
|
||||
|
||||
@@ -78,7 +78,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
const VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as const;
|
||||
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
|
||||
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
|
||||
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
|
||||
const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500;
|
||||
// Ignore transient "neither mpv nor overlay is the active window" blips before suppressing
|
||||
// subtitle pointer interaction. Right after playback starts the overlay can briefly become the
|
||||
@@ -93,7 +92,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let windowsOverlayPointerInteractionActive = false;
|
||||
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
||||
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
||||
let lastLinuxVisibleOverlayFollowedMpvAtMs = 0;
|
||||
const linuxPointerForegroundSuppressionGrace: ForegroundSuppressionGraceState = {
|
||||
lossSinceMs: null,
|
||||
@@ -482,15 +480,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
}
|
||||
}
|
||||
|
||||
function hasWindowsVisibleOverlayFocusHandoffGrace(): boolean {
|
||||
return (
|
||||
process.platform === 'win32' &&
|
||||
lastWindowsVisibleOverlayBlurredAtMs > 0 &&
|
||||
Date.now() - lastWindowsVisibleOverlayBlurredAtMs <=
|
||||
WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS
|
||||
);
|
||||
}
|
||||
|
||||
function shouldPollWindowsVisibleOverlayForegroundProcess(): boolean {
|
||||
if (process.platform !== 'win32' || !overlayManager.getVisibleOverlayVisible()) {
|
||||
return false;
|
||||
@@ -562,9 +551,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
lastWindowsVisibleOverlayBlurredAtMs = Date.now();
|
||||
}
|
||||
startMacOSVisibleOverlayForegroundProbe();
|
||||
clearVisibleOverlayBlurRefreshTimeouts();
|
||||
for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) {
|
||||
@@ -860,7 +846,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
retargetOverlayWindowTrackerForMpvSocket,
|
||||
requestWindowsVisibleOverlayZOrderSync,
|
||||
scheduleWindowsVisibleOverlayZOrderSyncBurst,
|
||||
hasWindowsVisibleOverlayFocusHandoffGrace,
|
||||
ensureWindowsVisibleOverlayForegroundPollLoop,
|
||||
clearWindowsVisibleOverlayForegroundPollLoop,
|
||||
scheduleVisibleOverlayBlurRefresh,
|
||||
@@ -881,8 +866,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
|
||||
windowsOverlayPointerInteractionActive = false;
|
||||
},
|
||||
getLinuxOverlayInputShapeActive: () => linuxOverlayInputShapeActive,
|
||||
getLastWindowsVisibleOverlayForegroundProcessName: () =>
|
||||
lastWindowsVisibleOverlayForegroundProcessName,
|
||||
getMacOSVisibleOverlayForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive,
|
||||
setLinuxOverlayInteractiveHint: (interactive: boolean) => {
|
||||
linuxOverlayInteractiveHint = interactive;
|
||||
|
||||
@@ -1464,7 +1464,7 @@ test('keyboard mode: an open modal receives h/l/arrows instead of lookup control
|
||||
});
|
||||
|
||||
test('keyboard mode: h/l/arrows typed into a text field are not consumed by lookup controls', async () => {
|
||||
const { ctx, testGlobals, handlers, setWordCount } = createKeyboardHandlerHarness();
|
||||
const { testGlobals, handlers, setWordCount } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
|
||||
@@ -31,12 +31,6 @@ export function createYoutubeTrackPickerModal(
|
||||
ctx.dom.youtubePickerStatus.style.color = isError ? '#ed8796' : '#a5adcb';
|
||||
}
|
||||
|
||||
function getTrackLabel(trackId: string): string {
|
||||
return (
|
||||
ctx.state.youtubePickerPayload?.tracks.find((track) => track.id === trackId)?.label ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
function renderTrackList(): void {
|
||||
ctx.dom.youtubePickerTracks.replaceChildren();
|
||||
const payload = ctx.state.youtubePickerPayload;
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
AiConfig,
|
||||
AiFeatureConfig,
|
||||
AnilistCharacterDictionaryCollapsibleSectionsConfig,
|
||||
AnilistCharacterDictionaryEvictionPolicy,
|
||||
AnilistCharacterDictionaryProfileScope,
|
||||
AnilistConfig,
|
||||
DiscordPresenceConfig,
|
||||
@@ -345,9 +344,7 @@ export interface ResolvedConfig {
|
||||
enabled: boolean;
|
||||
accessToken: string;
|
||||
characterDictionary: {
|
||||
refreshTtlHours: number;
|
||||
maxLoaded: number;
|
||||
evictionPolicy: AnilistCharacterDictionaryEvictionPolicy;
|
||||
profileScope: AnilistCharacterDictionaryProfileScope;
|
||||
collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>;
|
||||
};
|
||||
|
||||
@@ -50,7 +50,6 @@ export interface JimakuConfig {
|
||||
maxEntryResults?: number;
|
||||
}
|
||||
|
||||
export type AnilistCharacterDictionaryEvictionPolicy = 'disable' | 'delete';
|
||||
export type AnilistCharacterDictionaryProfileScope = 'all' | 'active';
|
||||
export type AnilistCharacterDictionaryCollapsibleSectionKey =
|
||||
| 'description'
|
||||
@@ -64,9 +63,7 @@ export interface AnilistCharacterDictionaryCollapsibleSectionsConfig {
|
||||
}
|
||||
|
||||
export interface AnilistCharacterDictionaryConfig {
|
||||
refreshTtlHours?: number;
|
||||
maxLoaded?: number;
|
||||
evictionPolicy?: AnilistCharacterDictionaryEvictionPolicy;
|
||||
profileScope?: AnilistCharacterDictionaryProfileScope;
|
||||
collapsibleSections?: AnilistCharacterDictionaryCollapsibleSectionsConfig;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ import type {
|
||||
SubtitleSidebarPlaybackState,
|
||||
SubtitleSidebarSnapshot,
|
||||
SubtitleRendererStyleConfig,
|
||||
SubtitleStyleConfig,
|
||||
} from './subtitle';
|
||||
import type {
|
||||
RuntimeOptionApplyResult,
|
||||
|
||||
@@ -13,7 +13,8 @@ const RECT = koffi.struct('RECT', {
|
||||
Bottom: 'int',
|
||||
});
|
||||
|
||||
const MARGINS = koffi.struct('MARGINS', {
|
||||
// Registered by name for the DwmExtendFrameIntoClientArea prototype below.
|
||||
koffi.struct('MARGINS', {
|
||||
cxLeftWidth: 'int',
|
||||
cxRightWidth: 'int',
|
||||
cyTopHeight: 'int',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false
|
||||
|
||||
Reference in new issue
Block a user