Merge remote-tracking branch 'origin/main' into add-hachidori-backend

# Conflicts:
#	src/main.ts
This commit is contained in:
sudacode committed 2026-10-12 22:14:30 -07:00
commit 8809ed38b2
81 files changed
+28 -2559

No files matched your search

+2 -113
View File
@@ -5,8 +5,6 @@
"": { "": {
"name": "subminer", "name": "subminer",
"dependencies": { "dependencies": {
"@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/geist-mono": "^5.2.7",
"@xhayper/discord-rpc": "^1.3.4", "@xhayper/discord-rpc": "^1.3.4",
"axios": "^1.20.0", "axios": "^1.20.0",
"commander": "^14.0.3", "commander": "^14.0.3",
@@ -24,7 +22,6 @@
"electron": "43.7.7", "electron": "43.7.7",
"electron-builder": "26.16.1", "electron-builder": "26.16.1",
"esbuild": "^0.25.12", "esbuild": "^0.25.12",
"eslint": "^10.8.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"undici": "7.29.1", "undici": "7.29.1",
@@ -127,34 +124,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@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=="], "@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=="], "@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/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/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/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/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=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
@@ -231,13 +194,9 @@
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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": ["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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], "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-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-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], "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=="], "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=="], "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": ["lodash@4.18.0", "", {}, "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "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=="], "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-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=="], "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=="], "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-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-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-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "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=="], "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=="], "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=="], "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=="], "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=="], "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
@@ -711,8 +612,6 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "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=="], "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=="], "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=="], "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=="], "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "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=="], "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=="], "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=="], "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=="], "@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=="], "@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=="], "@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/@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=="], "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=="], "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/@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=="], "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=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+4
View File
@@ -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.
+5
View File
@@ -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.
+4
View File
@@ -0,0 +1,4 @@
type: internal
area: overlay
- Removed 16 overlay preload/IPC methods that nothing called, plus their main-process handlers. SubMiner no longer observes mpv's `sub-text/ass` or sends every ASS line to overlay windows that never listened.
-3
View File
@@ -175,9 +175,6 @@ Run `make help` for the full list.
| `SUBMINER_MPV_PLUGIN_PATH` | mpv plugin directory the launcher injects | | `SUBMINER_MPV_PLUGIN_PATH` | mpv plugin directory the launcher injects |
| `SUBMINER_LOG_LEVEL` | App log level (`debug`, `info`, `warn`, `error`) | | `SUBMINER_LOG_LEVEL` | App log level (`debug`, `info`, `warn`, `error`) |
| `SUBMINER_MPV_LOG` | Shared mpv/app log file path | | `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_ACCESS_TOKEN` | Jellyfin access token, used before the stored encrypted session |
| `SUBMINER_JELLYFIN_USER_ID` | Jellyfin user ID | | `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` | | `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 { runConfigCommand } from './config-command.js';
import { runDictionaryCommand } from './dictionary-command.js'; import { runDictionaryCommand } from './dictionary-command.js';
import { runDoctorCommand } from './doctor-command.js'; import { runDoctorCommand } from './doctor-command.js';
import { runLogsCommand } from './logs-command.js';
import { runMpvPostAppCommand, runMpvPreAppCommand } from './mpv-command.js'; import { runMpvPostAppCommand, runMpvPreAppCommand } from './mpv-command.js';
import { runAppPassthroughCommand } from './app-command.js'; import { runAppPassthroughCommand } from './app-command.js';
import { runStatsCommand } from './stats-command.js'; import { runStatsCommand } from './stats-command.js';
import { runUpdateCommand } from './update-command.js';
class ExitSignal extends Error { class ExitSignal extends Error {
code: number; code: number;
+1 -1
View File
@@ -65,7 +65,7 @@ export async function runMpvPostAppCommand(
context: LauncherCommandContext, context: LauncherCommandContext,
deps: MpvCommandDeps = defaultDeps, deps: MpvCommandDeps = defaultDeps,
): Promise<boolean> { ): Promise<boolean> {
const { args, appPath, scriptPath, mpvSocketPath, pluginRuntimeConfig } = context; const { args, appPath, mpvSocketPath, pluginRuntimeConfig } = context;
if (!args.mpvIdle) { if (!args.mpvIdle) {
return false; return false;
} }
-9
View File
@@ -17,20 +17,11 @@ test('parseLauncherYoutubeSubgenConfig keeps only valid typed values', () => {
secondarySub: { secondarySub: {
secondarySubLanguages: ['eng', true, 'deu'], 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.equal('mode' in parsed, false);
assert.deepEqual(parsed.primarySubLanguages, ['ja', 'en']); assert.deepEqual(parsed.primarySubLanguages, ['ja', 'en']);
assert.deepEqual(parsed.secondarySubLanguages, ['eng', 'deu']); 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', () => { test('parseLauncherJellyfinConfig omits legacy token and user id fields', () => {
-15
View File
@@ -10,7 +10,6 @@ import type {
LogLevel, LogLevel,
} from '../types.js'; } from '../types.js';
import { import {
DEFAULT_JIMAKU_API_BASE_URL,
DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS, DEFAULT_YOUTUBE_PRIMARY_SUB_LANGS,
DEFAULT_YOUTUBE_SECONDARY_SUB_LANGS, DEFAULT_YOUTUBE_SECONDARY_SUB_LANGS,
} from '../types.js'; } from '../types.js';
@@ -125,11 +124,6 @@ export function createDefaultArgs(
recursive: false, recursive: false,
profile: mpvConfig.profile ?? '', profile: mpvConfig.profile ?? '',
startOverlay: false, 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, jellyfin: false,
jellyfinLogin: false, jellyfinLogin: false,
jellyfinLogout: false, jellyfinLogout: false,
@@ -184,15 +178,6 @@ export function createDefaultArgs(
targetKind: '', 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; return parsed;
} }
-22
View File
@@ -16,31 +16,9 @@ export function parseLauncherYoutubeSubgenConfig(
secondarySubRaw && typeof secondarySubRaw === 'object' secondarySubRaw && typeof secondarySubRaw === 'object'
? (secondarySubRaw as Record<string, unknown>) ? (secondarySubRaw as Record<string, unknown>)
: null; : 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 { return {
primarySubLanguages: asStringArray(youtube?.primarySubLanguages), primarySubLanguages: asStringArray(youtube?.primarySubLanguages),
secondarySubLanguages: asStringArray(secondarySub?.secondarySubLanguages), 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,
}; };
} }
-518
View File
@@ -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,
};
}
-1
View File
@@ -543,7 +543,6 @@ test(
const appStartEntries = readJsonLines(appStartPath); const appStartEntries = readJsonLines(appStartPath);
const appStopEntries = readJsonLines(appStopPath); const appStopEntries = readJsonLines(appStopPath);
const controlEntries = readJsonLines(controlServer.logPath); const controlEntries = readJsonLines(controlServer.logPath);
const mpvEntries = readJsonLines(path.join(smokeCase.artifactsDir, 'fake-mpv.log'));
assert.equal(result.status, 0); assert.equal(result.status, 0);
if (process.platform === 'linux') { if (process.platform === 'linux') {
-12
View File
@@ -52,7 +52,6 @@ export function getDefaultMpvLogFile(options?: {
export const DEFAULT_MPV_LOG_FILE = getDefaultMpvLogFile(); export const DEFAULT_MPV_LOG_FILE = getDefaultMpvLogFile();
export const DEFAULT_YOUTUBE_YTDL_FORMAT = 'bestvideo*+bestaudio/best'; 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 = [ export const DEFAULT_MPV_SUBMINER_ARGS = [
'--sub-auto=fuzzy', '--sub-auto=fuzzy',
'--sub-file-paths=.;subs;subtitles', '--sub-file-paths=.;subs;subtitles',
@@ -69,7 +68,6 @@ export function shouldForwardLogLevel(level: LogLevel): boolean {
return level === 'debug' || level === 'error'; return level === 'debug' || level === 'error';
} }
export type Backend = 'auto' | 'hyprland' | 'sway' | 'x11' | 'macos' | 'windows'; export type Backend = 'auto' | 'hyprland' | 'sway' | 'x11' | 'macos' | 'windows';
export type JimakuLanguagePreference = 'ja' | 'en' | 'none';
export interface Args { export interface Args {
generateSubtitles?: { generateSubtitles?: {
@@ -103,11 +101,6 @@ export interface Args {
passwordStore: string; passwordStore: string;
target: string; target: string;
targetKind: '' | 'file' | 'url'; targetKind: '' | 'file' | 'url';
jimakuApiKey: string;
jimakuApiKeyCommand: string;
jimakuApiBaseUrl: string;
jimakuLanguagePreference: JimakuLanguagePreference;
jimakuMaxEntryResults: number;
jellyfin: boolean; jellyfin: boolean;
jellyfinLogin: boolean; jellyfinLogin: boolean;
jellyfinLogout: boolean; jellyfinLogout: boolean;
@@ -151,11 +144,6 @@ export interface Args {
export interface LauncherYoutubeSubgenConfig { export interface LauncherYoutubeSubgenConfig {
primarySubLanguages?: string[]; primarySubLanguages?: string[];
secondarySubLanguages?: string[]; secondarySubLanguages?: string[];
jimakuApiKey?: string;
jimakuApiKeyCommand?: string;
jimakuApiBaseUrl?: string;
jimakuLanguagePreference?: JimakuLanguagePreference;
jimakuMaxEntryResults?: number;
} }
export interface LauncherJellyfinConfig { export interface LauncherJellyfinConfig {
-3
View File
@@ -116,8 +116,6 @@
"author": "", "author": "",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"@fontsource-variable/geist": "^5.2.8",
"@fontsource-variable/geist-mono": "^5.2.7",
"@xhayper/discord-rpc": "^1.3.4", "@xhayper/discord-rpc": "^1.3.4",
"axios": "^1.20.0", "axios": "^1.20.0",
"commander": "^14.0.3", "commander": "^14.0.3",
@@ -135,7 +133,6 @@
"electron": "43.7.7", "electron": "43.7.7",
"electron-builder": "26.16.1", "electron-builder": "26.16.1",
"esbuild": "^0.25.12", "esbuild": "^0.25.12",
"eslint": "^10.8.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"undici": "7.29.1" "undici": "7.29.1"
-3
View File
@@ -56,9 +56,6 @@ function M.init()
ctx.binary = make_lazy_proxy("binary", function() ctx.binary = make_lazy_proxy("binary", function()
return require("binary").create(ctx) return require("binary").create(ctx)
end) end)
ctx.hover = make_lazy_proxy("hover", function()
return require("hover").create(ctx)
end)
ctx.process = make_lazy_proxy("process", function() ctx.process = make_lazy_proxy("process", function()
return require("process").create(ctx) return require("process").create(ctx)
end) end)
-430
View File
@@ -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
-15
View File
@@ -10,7 +10,6 @@ function M.create(ctx)
local state = ctx.state local state = ctx.state
local options_helper = ctx.options_helper local options_helper = ctx.options_helper
local process = ctx.process local process = ctx.process
local hover = ctx.hover
local subminer_log = ctx.log.subminer_log local subminer_log = ctx.log.subminer_log
local show_osd = ctx.log.show_osd local show_osd = ctx.log.show_osd
@@ -308,7 +307,6 @@ function M.create(ctx)
local function on_shutdown() local function on_shutdown()
next_auto_start_retry_generation() next_auto_start_retry_generation()
hover.clear_hover_overlay()
process.disarm_auto_play_ready_gate() process.disarm_auto_play_ready_gate()
process.stop_overlay_loading_osd() process.stop_overlay_loading_osd()
clear_pending_visible_overlay_hide() clear_pending_visible_overlay_hide()
@@ -326,13 +324,9 @@ function M.create(ctx)
mp.register_event("start-file", on_start_file) mp.register_event("start-file", on_start_file)
mp.register_event("file-loaded", on_file_loaded) mp.register_event("file-loaded", on_file_loaded)
mp.register_event("shutdown", on_shutdown) mp.register_event("shutdown", on_shutdown)
mp.register_event("file-loaded", function()
hover.clear_hover_overlay()
end)
mp.register_event("end-file", function(event) mp.register_event("end-file", function(event)
process.stop_overlay_loading_osd() process.stop_overlay_loading_osd()
process.disarm_auto_play_ready_gate() process.disarm_auto_play_ready_gate()
hover.clear_hover_overlay()
local reason = type(event) == "table" and event.reason or nil local reason = type(event) == "table" and event.reason or nil
if is_reload_end_file(reason) then if is_reload_end_file(reason) then
state.pending_reload_media_identity = state.current_media_identity or resolve_media_identity() 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() hide_visible_overlay_after_end_file()
end end
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 end
return { return {
-7
View File
@@ -4,7 +4,6 @@ function M.create(ctx)
local mp = ctx.mp local mp = ctx.mp
local opts = ctx.opts local opts = ctx.opts
local process = ctx.process local process = ctx.process
local hover = ctx.hover
local ui = ctx.ui local ui = ctx.ui
local state = ctx.state local state = ctx.state
@@ -49,12 +48,6 @@ function M.create(ctx)
mp.register_script_message("subminer-overlay-loading-ready", function() mp.register_script_message("subminer-overlay-loading-ready", function()
process.stop_overlay_loading_osd() process.stop_overlay_loading_osd()
end) 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() mp.register_script_message("subminer-stats-toggle", function()
if opts.osd_messages then if opts.osd_messages then
mp.osd_message("Stats: press ` (backtick) in overlay", 3) mp.osd_message("Stats: press ` (backtick) in overlay", 3)
-10
View File
@@ -8,16 +8,6 @@ function M.new()
binary_available = false, binary_available = false,
binary_path = nil, binary_path = nil,
detected_backend = 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_gate_armed = false,
auto_play_ready_should_resume_playback = false, auto_play_ready_should_resume_playback = false,
auto_play_ready_timeout = nil, auto_play_ready_timeout = nil,
-1
View File
@@ -1,6 +1,5 @@
local MODULE_PATHS = { local MODULE_PATHS = {
"plugin/subminer/bootstrap.lua", "plugin/subminer/bootstrap.lua",
"plugin/subminer/hover.lua",
"plugin/subminer/environment.lua", "plugin/subminer/environment.lua",
"plugin/subminer/version.lua", "plugin/subminer/version.lua",
} }
-1
View File
@@ -267,7 +267,6 @@ local function run_plugin_scenario(config)
package.loaded["binary"] = nil package.loaded["binary"] = nil
package.loaded["bootstrap"] = nil package.loaded["bootstrap"] = nil
package.loaded["environment"] = nil package.loaded["environment"] = nil
package.loaded["hover"] = nil
package.loaded["init"] = nil package.loaded["init"] = nil
package.loaded["lifecycle"] = nil package.loaded["lifecycle"] = nil
package.loaded["log"] = nil package.loaded["log"] = nil
-5
View File
@@ -41,7 +41,6 @@ import type { NPlusOneMatchMode, SubtitleMiningContext } from './types/subtitle'
import { DEFAULT_ANKI_CONNECT_CONFIG } from './config'; import { DEFAULT_ANKI_CONNECT_CONFIG } from './config';
import { import {
getConfiguredWordFieldCandidates, getConfiguredWordFieldCandidates,
getConfiguredWordFieldName,
getPreferredWordValueFromExtractedFields, getPreferredWordValueFromExtractedFields,
} from './anki-field-config'; } from './anki-field-config';
import { createLogger } from './logger'; import { createLogger } from './logger';
@@ -1632,10 +1631,6 @@ export class AnkiIntegration {
return sentenceCardConfig.audioField || 'SentenceAudio'; return sentenceCardConfig.audioField || 'SentenceAudio';
} }
private getConfiguredWordFieldName(): string {
return getConfiguredWordFieldName(this.config);
}
private getConfiguredWordFieldCandidates(): string[] { private getConfiguredWordFieldCandidates(): string[] {
return getConfiguredWordFieldCandidates(this.config); return getConfiguredWordFieldCandidates(this.config);
} }
+1 -13
View File
@@ -1,10 +1,6 @@
import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config'; import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
import type { AnkiConnectConfig } from '../types/anki'; import type { AnkiConnectConfig } from '../types/anki';
import { import { getKnownWordCacheLifecycleConfig } from './known-word-cache';
getKnownWordCacheLifecycleConfig,
getKnownWordCacheRefreshIntervalMinutes,
getKnownWordCacheScopeForConfig,
} from './known-word-cache';
export interface AnkiIntegrationRuntimeProxyServer { export interface AnkiIntegrationRuntimeProxyServer {
start(options: { host: string; port: number; upstreamUrl: string }): void; start(options: { host: string; port: number; upstreamUrl: string }): void;
@@ -255,14 +251,6 @@ export class AnkiIntegrationRuntime {
return getKnownWordCacheLifecycleConfig(config); return getKnownWordCacheLifecycleConfig(config);
} }
private getKnownWordRefreshIntervalMinutes(config: AnkiConnectConfig): number {
return getKnownWordCacheRefreshIntervalMinutes(config);
}
private getKnownWordCacheScopeForConfig(config: AnkiConnectConfig): string {
return getKnownWordCacheScopeForConfig(config);
}
getOrCreateProxyServer(): AnkiIntegrationRuntimeProxyServer { getOrCreateProxyServer(): AnkiIntegrationRuntimeProxyServer {
if (!this.proxyServer) { if (!this.proxyServer) {
this.proxyServer = this.deps.proxyServerFactory(); this.proxyServer = this.deps.proxyServerFactory();
@@ -133,9 +133,7 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
enabled: false, enabled: false,
accessToken: '', accessToken: '',
characterDictionary: { characterDictionary: {
refreshTtlHours: 168,
maxLoaded: 3, maxLoaded: 3,
evictionPolicy: 'delete',
profileScope: 'all', profileScope: 'all',
collapsibleSections: { collapsibleSections: {
description: false, description: false,
@@ -37,8 +37,6 @@ function collectConfigLeafPaths(config: ResolvedConfig): string[] {
// an allowlist entry. Only allowlist a path when the registry is genuinely // an allowlist entry. Only allowlist a path when the registry is genuinely
// the wrong surface for it. // the wrong surface for it.
const UNDOCUMENTED_LEAVES: ReadonlySet<string> = new Set([ const UNDOCUMENTED_LEAVES: ReadonlySet<string> = new Set([
'anilist.characterDictionary.evictionPolicy',
'anilist.characterDictionary.refreshTtlHours',
'keybindings', 'keybindings',
'subtitleStyle.backdropFilter', 'subtitleStyle.backdropFilter',
'subtitleStyle.backgroundColor', 'subtitleStyle.backgroundColor',
-43
View File
@@ -94,27 +94,6 @@ export function applyIntegrationConfig(context: ResolveContext): void {
if (isObject(src.anilist.characterDictionary)) { if (isObject(src.anilist.characterDictionary)) {
const characterDictionary = 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); const maxLoaded = asNumber(characterDictionary.maxLoaded);
if (maxLoaded !== undefined) { if (maxLoaded !== undefined) {
const normalized = Math.min(20, Math.max(1, Math.floor(maxLoaded))); 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); const profileScopeRaw = asString(characterDictionary.profileScope);
if (profileScopeRaw !== undefined) { if (profileScopeRaw !== undefined) {
const profileScope = profileScopeRaw.trim().toLowerCase(); const profileScope = profileScopeRaw.trim().toLowerCase();
+1 -7
View File
@@ -97,9 +97,7 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
const { context, warnings } = createResolveContext({ const { context, warnings } = createResolveContext({
anilist: { anilist: {
characterDictionary: { characterDictionary: {
refreshTtlHours: 0,
maxLoaded: 99, maxLoaded: 99,
evictionPolicy: 'purge' as never,
profileScope: 'global' as never, profileScope: 'global' as never,
collapsibleSections: { collapsibleSections: {
description: true, description: true,
@@ -112,9 +110,7 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
applyIntegrationConfig(context); applyIntegrationConfig(context);
assert.equal(context.resolved.anilist.characterDictionary.refreshTtlHours, 1);
assert.equal(context.resolved.anilist.characterDictionary.maxLoaded, 20); 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.profileScope, 'all');
assert.equal(context.resolved.anilist.characterDictionary.collapsibleSections.description, true); assert.equal(context.resolved.anilist.characterDictionary.collapsibleSections.description, true);
assert.equal( 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); assert.equal(context.resolved.anilist.characterDictionary.collapsibleSections.voicedBy, true);
const warnedPaths = warnings.map((warning) => warning.path); 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.maxLoaded'));
assert.ok(warnedPaths.includes('anilist.characterDictionary.evictionPolicy'));
assert.ok(warnedPaths.includes('anilist.characterDictionary.profileScope')); assert.ok(warnedPaths.includes('anilist.characterDictionary.profileScope'));
assert.ok( assert.ok(
warnedPaths.includes('anilist.characterDictionary.collapsibleSections.characterInformation'), 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', () => { test('yomitan externalProfilePath is trimmed and invalid values warn', () => {
const { context, warnings } = createResolveContext({ const { context } = createResolveContext({
yomitan: { yomitan: {
externalProfilePath: ' /tmp/gsm-profile ', externalProfilePath: ' /tmp/gsm-profile ',
}, },
-7
View File
@@ -18,10 +18,6 @@ const TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY = new Map(
CONFIG_TEMPLATE_SECTIONS.map((section) => [String(section.key), section.description[0] ?? '']), CONFIG_TEMPLATE_SECTIONS.map((section) => [String(section.key), section.description[0] ?? '']),
); );
const SUBTITLE_CSS_SCOPES: SubtitleCssScope[] = ['primary', 'secondary', 'sidebar']; const SUBTITLE_CSS_SCOPES: SubtitleCssScope[] = ['primary', 'secondary', 'sidebar'];
const HIDDEN_TEMPLATE_PATHS = [
'anilist.characterDictionary.evictionPolicy',
'anilist.characterDictionary.refreshTtlHours',
];
function normalizeCommentText(value: string): string { function normalizeCommentText(value: string): string {
return value.replace(/\s+/g, ' ').replace(/\*\//g, '*\\/').trim(); return value.replace(/\s+/g, ' ').replace(/\*\//g, '*\\/').trim();
@@ -176,9 +172,6 @@ function renderSection(
function createTemplateConfig(config: ResolvedConfig): ResolvedConfig { function createTemplateConfig(config: ResolvedConfig): ResolvedConfig {
const templateConfig = deepCloneConfig(config); const templateConfig = deepCloneConfig(config);
foldSubtitleCssManagedDefaults(templateConfig); foldSubtitleCssManagedDefaults(templateConfig);
for (const hiddenPath of HIDDEN_TEMPLATE_PATHS) {
deleteValueAtPath(templateConfig, hiddenPath);
}
if (templateConfig.keybindings.length === 0) { if (templateConfig.keybindings.length === 0) {
templateConfig.keybindings = DEFAULT_KEYBINDINGS.map((binding) => ({ templateConfig.keybindings = DEFAULT_KEYBINDINGS.map((binding) => ({
key: binding.key, key: binding.key,
@@ -87,20 +87,6 @@ export interface GuessAnilistMediaInfoDeps {
runGuessit: (target: string) => Promise<string>; 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 { function normalizeGuessitTitlePart(value: string): string {
return value.replace(/[._]+/g, ' ').replace(/-/g, ' ').replace(/\s+/g, ' ').trim(); return value.replace(/[._]+/g, ' ').replace(/-/g, ' ').replace(/\s+/g, ' ').trim();
} }
-13
View File
@@ -32,8 +32,6 @@ test('anki/jimaku IPC handlers reject malformed invoke payloads', async () => {
let previewCalls = 0; let previewCalls = 0;
registerAnkiJimakuIpcHandlers( registerAnkiJimakuIpcHandlers(
{ {
setAnkiConnectEnabled: () => {},
clearAnkiHistory: () => {},
refreshKnownWords: async () => {}, refreshKnownWords: async () => {},
respondFieldGrouping: () => {}, respondFieldGrouping: () => {},
buildKikuMergePreview: async () => { buildKikuMergePreview: async () => {
@@ -138,8 +136,6 @@ test('tsukihime downloads always route Japanese as primary', async () => {
const secondaryLoads: string[] = []; const secondaryLoads: string[] = [];
registerAnkiJimakuIpcHandlers( registerAnkiJimakuIpcHandlers(
{ {
setAnkiConnectEnabled: () => {},
clearAnkiHistory: () => {},
refreshKnownWords: async () => {}, refreshKnownWords: async () => {},
respondFieldGrouping: () => {}, respondFieldGrouping: () => {},
buildKikuMergePreview: async () => ({ ok: true }), buildKikuMergePreview: async () => ({ ok: true }),
@@ -205,13 +201,8 @@ test('tsukihime downloads always route Japanese as primary', async () => {
test('anki/jimaku IPC command handlers ignore malformed payloads', () => { test('anki/jimaku IPC command handlers ignore malformed payloads', () => {
const { registrar, onHandlers } = createFakeRegistrar(); const { registrar, onHandlers } = createFakeRegistrar();
const fieldGroupingChoices: unknown[] = []; const fieldGroupingChoices: unknown[] = [];
const enabledStates: boolean[] = [];
registerAnkiJimakuIpcHandlers( registerAnkiJimakuIpcHandlers(
{ {
setAnkiConnectEnabled: (enabled) => {
enabledStates.push(enabled);
},
clearAnkiHistory: () => {},
refreshKnownWords: async () => {}, refreshKnownWords: async () => {},
respondFieldGrouping: (choice) => { respondFieldGrouping: (choice) => {
fieldGroupingChoices.push(choice); fieldGroupingChoices.push(choice);
@@ -241,10 +232,6 @@ test('anki/jimaku IPC command handlers ignore malformed payloads', () => {
registrar, registrar,
); );
onHandlers.get(IPC_CHANNELS.command.setAnkiConnectEnabled)!({}, 'true');
onHandlers.get(IPC_CHANNELS.command.setAnkiConnectEnabled)!({}, true);
assert.deepEqual(enabledStates, [true]);
onHandlers.get(IPC_CHANNELS.command.kikuFieldGroupingRespond)!({}, null); onHandlers.get(IPC_CHANNELS.command.kikuFieldGroupingRespond)!({}, null);
onHandlers.get(IPC_CHANNELS.command.kikuFieldGroupingRespond)!( onHandlers.get(IPC_CHANNELS.command.kikuFieldGroupingRespond)!(
{}, {},
-11
View File
@@ -40,8 +40,6 @@ const { ipcMain } = electron;
const logger = createLogger('main:anki-jimaku-ipc'); const logger = createLogger('main:anki-jimaku-ipc');
export interface AnkiJimakuIpcDeps { export interface AnkiJimakuIpcDeps {
setAnkiConnectEnabled: (enabled: boolean) => void;
clearAnkiHistory: () => void;
refreshKnownWords: () => Promise<void> | void; refreshKnownWords: () => Promise<void> | void;
respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void; respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void;
buildKikuMergePreview: (request: KikuMergePreviewRequest) => Promise<KikuMergePreviewResponse>; buildKikuMergePreview: (request: KikuMergePreviewRequest) => Promise<KikuMergePreviewResponse>;
@@ -77,15 +75,6 @@ export function registerAnkiJimakuIpcHandlers(
deps: AnkiJimakuIpcDeps, deps: AnkiJimakuIpcDeps,
ipc: IpcMainRegistrar = ipcMain, ipc: IpcMainRegistrar = ipcMain,
): void { ): void {
ipc.on(IPC_CHANNELS.command.setAnkiConnectEnabled, (_event: unknown, enabled: unknown) => {
if (typeof enabled !== 'boolean') return;
deps.setAnkiConnectEnabled(enabled);
});
ipc.on(IPC_CHANNELS.command.clearAnkiConnectHistory, () => {
deps.clearAnkiHistory();
});
ipc.on(IPC_CHANNELS.command.refreshKnownWords, async () => { ipc.on(IPC_CHANNELS.command.refreshKnownWords, async () => {
await deps.refreshKnownWords(); await deps.refreshKnownWords();
}); });
+1 -54
View File
@@ -8,8 +8,6 @@ interface RuntimeHarness {
state: { state: {
ankiIntegration: unknown; ankiIntegration: unknown;
fieldGroupingResolver: ((choice: unknown) => void) | null; fieldGroupingResolver: ((choice: unknown) => void) | null;
patches: boolean[];
broadcasts: number;
fetchCalls: Array<{ endpoint: string; query?: Record<string, unknown> }>; fetchCalls: Array<{ endpoint: string; query?: Record<string, unknown> }>;
tsukihimeFetchCalls: Array<{ endpoint: string; query?: Record<string, unknown> }>; tsukihimeFetchCalls: Array<{ endpoint: string; query?: Record<string, unknown> }>;
sentCommands: Array<{ command: (string | number)[] }>; sentCommands: Array<{ command: (string | number)[] }>;
@@ -20,8 +18,6 @@ function createHarness(): RuntimeHarness {
const state = { const state = {
ankiIntegration: null as unknown, ankiIntegration: null as unknown,
fieldGroupingResolver: null as ((choice: unknown) => void) | null, fieldGroupingResolver: null as ((choice: unknown) => void) | null,
patches: [] as boolean[],
broadcasts: 0,
fetchCalls: [] as Array<{ fetchCalls: [] as Array<{
endpoint: string; endpoint: string;
query?: Record<string, unknown>; query?: Record<string, unknown>;
@@ -34,11 +30,7 @@ function createHarness(): RuntimeHarness {
}; };
const options: AnkiJimakuIpcRuntimeOptions = { const options: AnkiJimakuIpcRuntimeOptions = {
patchAnkiConnectEnabled: (enabled) => {
state.patches.push(enabled);
},
getResolvedConfig: () => ({ tsukihime: { maxSearchResults: 2 } }), getResolvedConfig: () => ({ tsukihime: { maxSearchResults: 2 } }),
getRuntimeOptionsManager: () => null,
tsukihimeFetchJson: async (endpoint, query) => { tsukihimeFetchJson: async (endpoint, query) => {
state.tsukihimeFetchCalls.push({ state.tsukihimeFetchCalls.push({
endpoint, endpoint,
@@ -76,7 +68,6 @@ function createHarness(): RuntimeHarness {
} as never, } as never,
}; };
}, },
getSubtitleTimingTracker: () => null,
getMpvClient: () => ({ getMpvClient: () => ({
connected: true, connected: true,
send: (payload) => { send: (payload) => {
@@ -99,20 +90,6 @@ function createHarness(): RuntimeHarness {
}, },
}), }),
getAnkiIntegration: () => state.ankiIntegration as never, getAnkiIntegration: () => state.ankiIntegration as never,
setAnkiIntegration: (integration) => {
state.ankiIntegration = integration;
},
getKnownWordCacheStatePath: () => '/tmp/subminer-known-words-cache.json',
showDesktopNotification: () => {},
createFieldGroupingCallback: () => async () => ({
keepNoteId: 1,
deleteNoteId: 2,
deleteDuplicate: false,
cancelled: false,
}),
broadcastRuntimeOptionsChanged: () => {
state.broadcasts += 1;
},
getFieldGroupingResolver: () => state.fieldGroupingResolver as never, getFieldGroupingResolver: () => state.fieldGroupingResolver as never,
setFieldGroupingResolver: (resolver) => { setFieldGroupingResolver: (resolver) => {
state.fieldGroupingResolver = resolver as never; state.fieldGroupingResolver = resolver as never;
@@ -169,51 +146,21 @@ test('refreshKnownWords throws when integration is unavailable', async () => {
); );
}); });
test('setAnkiConnectEnabled disables active integration and broadcasts changes', () => { test('respondFieldGrouping resolves and clears the pending resolver', () => {
const { registered, state } = createHarness(); const { registered, state } = createHarness();
let destroyed = 0;
state.ankiIntegration = {
destroy: () => {
destroyed += 1;
},
};
registered.setAnkiConnectEnabled!(false);
assert.deepEqual(state.patches, [false]);
assert.equal(destroyed, 1);
assert.equal(state.ankiIntegration, null);
assert.equal(state.broadcasts, 1);
});
test('clearAnkiHistory and respondFieldGrouping execute runtime callbacks', () => {
const { registered, state, options } = createHarness();
let cleaned = 0;
let resolvedChoice: unknown = null; let resolvedChoice: unknown = null;
state.fieldGroupingResolver = (choice) => { state.fieldGroupingResolver = (choice) => {
resolvedChoice = choice; resolvedChoice = choice;
}; };
const originalGetTracker = options.getSubtitleTimingTracker;
options.getSubtitleTimingTracker = () =>
({
cleanup: () => {
cleaned += 1;
},
}) as never;
const choice = { const choice = {
keepNoteId: 10, keepNoteId: 10,
deleteNoteId: 11, deleteNoteId: 11,
deleteDuplicate: true, deleteDuplicate: true,
cancelled: false, cancelled: false,
}; };
registered.clearAnkiHistory!();
registered.respondFieldGrouping!(choice); registered.respondFieldGrouping!(choice);
options.getSubtitleTimingTracker = originalGetTracker;
assert.equal(cleaned, 1);
assert.deepEqual(resolvedChoice, choice); assert.deepEqual(resolvedChoice, choice);
assert.equal(state.fieldGroupingResolver, null); assert.equal(state.fieldGroupingResolver, null);
}); });
+1 -86
View File
@@ -1,19 +1,14 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { AnkiIntegration } from '../../anki-integration'; import type { AnkiIntegration } from '../../anki-integration';
import { mergeAiConfig } from '../../ai/config';
import { import {
AiConfig,
TsukihimeApiResponse, TsukihimeApiResponse,
TsukihimeConfig, TsukihimeConfig,
AnkiConnectConfig,
JimakuApiResponse, JimakuApiResponse,
JimakuEntry, JimakuEntry,
JimakuFileEntry, JimakuFileEntry,
JimakuLanguagePreference, JimakuLanguagePreference,
JimakuMediaInfo, JimakuMediaInfo,
KikuFieldGroupingChoice, KikuFieldGroupingChoice,
KikuFieldGroupingRequestData,
OverlayNotificationPayload,
} from '../../types'; } from '../../types';
import { sortJimakuFiles } from '../../jimaku/utils'; import { sortJimakuFiles } from '../../jimaku/utils';
import { import {
@@ -35,41 +30,13 @@ interface MpvClientLike {
request?: (command: unknown[]) => Promise<{ data?: unknown }>; request?: (command: unknown[]) => Promise<{ data?: unknown }>;
} }
interface RuntimeOptionsManagerLike {
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
}
interface SubtitleTimingTrackerLike {
cleanup: () => void;
}
export interface AnkiJimakuIpcRuntimeOptions { export interface AnkiJimakuIpcRuntimeOptions {
patchAnkiConnectEnabled: (enabled: boolean) => void;
getResolvedConfig: () => { getResolvedConfig: () => {
ankiConnect?: AnkiConnectConfig;
ai?: AiConfig;
tsukihime?: TsukihimeConfig; tsukihime?: TsukihimeConfig;
secondarySub?: { secondarySubLanguages?: string[] }; secondarySub?: { secondarySubLanguages?: string[] };
}; };
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
getSubtitleTimingTracker: () => SubtitleTimingTrackerLike | null;
getMpvClient: () => MpvClientLike | null; getMpvClient: () => MpvClientLike | null;
getAnkiIntegration: () => AnkiIntegration | null; getAnkiIntegration: () => AnkiIntegration | null;
setAnkiIntegration: (integration: AnkiIntegration | null) => void;
getKnownWordCacheStatePath: () => string;
getCachedMediaPath?: (
currentVideoPath: string,
kind: 'audio' | 'video',
) => Promise<string | null>;
shouldRequireRemoteMediaCache?: () => boolean;
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined;
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
dismissOverlayNotification?: (id: string) => void;
createFieldGroupingCallback: () => (
data: KikuFieldGroupingRequestData,
) => Promise<KikuFieldGroupingChoice>;
broadcastRuntimeOptionsChanged: () => void;
getFieldGroupingResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; getFieldGroupingResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setFieldGroupingResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; setFieldGroupingResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
parseMediaInfo: (mediaPath: string | null) => JimakuMediaInfo; parseMediaInfo: (mediaPath: string | null) => JimakuMediaInfo;
@@ -135,58 +102,6 @@ export function registerAnkiJimakuIpcRuntime(
registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler, registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
): void { ): void {
registerHandlers({ registerHandlers({
setAnkiConnectEnabled: (enabled) => {
options.patchAnkiConnectEnabled(enabled);
const config = options.getResolvedConfig();
const subtitleTimingTracker = options.getSubtitleTimingTracker();
const mpvClient = options.getMpvClient();
const ankiIntegration = options.getAnkiIntegration();
if (enabled && !ankiIntegration && subtitleTimingTracker && mpvClient) {
const runtimeOptionsManager = options.getRuntimeOptionsManager();
const effectiveAnkiConfig = runtimeOptionsManager
? runtimeOptionsManager.getEffectiveAnkiConnectConfig(config.ankiConnect)
: config.ankiConnect;
const integration = new AnkiIntegration(
effectiveAnkiConfig as never,
subtitleTimingTracker as never,
mpvClient as never,
(text: string) => {
if (mpvClient) {
mpvClient.send({
command: ['show-text', text, '3000'],
});
}
},
options.showDesktopNotification,
options.createFieldGroupingCallback(),
options.getKnownWordCacheStatePath(),
mergeAiConfig(config.ai, config.ankiConnect?.ai) as AiConfig,
undefined,
options.showOverlayNotification,
options.getCachedMediaPath,
options.shouldRequireRemoteMediaCache,
options.getYoutubeMediaSourceUrl,
options.dismissOverlayNotification,
);
integration.start();
options.setAnkiIntegration(integration);
logger.info('AnkiConnect integration enabled');
} else if (!enabled && ankiIntegration) {
ankiIntegration.destroy();
options.setAnkiIntegration(null);
logger.info('AnkiConnect integration disabled');
}
options.broadcastRuntimeOptionsChanged();
},
clearAnkiHistory: () => {
const subtitleTimingTracker = options.getSubtitleTimingTracker();
if (subtitleTimingTracker) {
subtitleTimingTracker.cleanup();
logger.info('AnkiConnect subtitle timing history cleared');
}
},
refreshKnownWords: async () => { refreshKnownWords: async () => {
const integration = options.getAnkiIntegration(); const integration = options.getAnkiIntegration();
if (!integration) { if (!integration) {
@@ -147,16 +147,10 @@ import { enqueueWrite } from './immersion-tracker/queue';
import { nowMs } from './immersion-tracker/time'; import { nowMs } from './immersion-tracker/time';
import { import {
DEFAULT_BATCH_SIZE, DEFAULT_BATCH_SIZE,
DEFAULT_DAILY_ROLLUP_RETENTION_MS,
DEFAULT_EVENTS_RETENTION_MS,
DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_FLUSH_INTERVAL_MS,
DEFAULT_MAINTENANCE_INTERVAL_MS, DEFAULT_MAINTENANCE_INTERVAL_MS,
DEFAULT_MAX_PAYLOAD_BYTES, DEFAULT_MAX_PAYLOAD_BYTES,
DEFAULT_MONTHLY_ROLLUP_RETENTION_MS,
DEFAULT_QUEUE_CAP, DEFAULT_QUEUE_CAP,
DEFAULT_SESSIONS_RETENTION_MS,
DEFAULT_TELEMETRY_RETENTION_MS,
DEFAULT_VACUUM_INTERVAL_MS,
EVENT_CARD_MINED, EVENT_CARD_MINED,
EVENT_LOOKUP, EVENT_LOOKUP,
EVENT_MEDIA_BUFFER, EVENT_MEDIA_BUFFER,
@@ -2769,7 +2769,6 @@ test('getSessionWordsByLine joins word occurrences through imm_words.id', () =>
try { try {
ensureSchema(db); ensureSchema(db);
const stmts = createTrackerPreparedStatements(db);
const startedAtMs = 1_735_732_800_000; const startedAtMs = 1_735_732_800_000;
const videoId = getOrCreateVideoRecord(db, '/tmp/session-words-by-line.mkv', { const videoId = getOrCreateVideoRecord(db, '/tmp/session-words-by-line.mkv', {
canonicalTitle: 'Episode', canonicalTitle: 'Episode',
@@ -18,7 +18,6 @@ import type {
} from './types'; } from './types';
import { import {
ACTIVE_SESSION_METRICS_CTE, ACTIVE_SESSION_METRICS_CTE,
SESSION_WORD_COUNTS_CTE,
SESSION_WORD_COUNTS_SELECT, SESSION_WORD_COUNTS_SELECT,
fromDbTimestamp, fromDbTimestamp,
resolvedCoverBlobExpr, resolvedCoverBlobExpr,
@@ -16,7 +16,6 @@ import { deriveStoredPartOfSpeech } from '../tokenizer/part-of-speech';
import { import {
cleanupUnusedCoverArtBlobHash, cleanupUnusedCoverArtBlobHash,
findSharedCoverBlobHash, findSharedCoverBlobHash,
toDbMs,
toDbTimestamp, toDbTimestamp,
} from './query-shared'; } from './query-shared';
@@ -9,7 +9,7 @@ import { ensureLexicalDailyRollupTables, markLexicalDailyRollupsReady } from './
import { isVocabularyStatsRowVisible, type VocabularyVisibilityRow } from './vocabulary-visibility'; import { isVocabularyStatsRowVisible, type VocabularyVisibilityRow } from './vocabulary-visibility';
import { SCHEMA_VERSION } from './types'; import { SCHEMA_VERSION } from './types';
import type { QueuedWrite, VideoMetadata, YoutubeVideoMetadata } from './types'; import type { QueuedWrite, VideoMetadata, YoutubeVideoMetadata } from './types';
import { toDbMs, toDbTimestamp } from './query-shared'; import { toDbTimestamp } from './query-shared';
export interface TrackerPreparedStatements { export interface TrackerPreparedStatements {
telemetryInsertStmt: ReturnType<DatabaseSync['prepare']>; telemetryInsertStmt: ReturnType<DatabaseSync['prepare']>;
-213
View File
@@ -114,23 +114,15 @@ function createSubtitleSidebarSnapshotFixture(): SubtitleSidebarSnapshot {
function createRegisterIpcDeps(overrides: Partial<IpcServiceDeps> = {}): IpcServiceDeps { function createRegisterIpcDeps(overrides: Partial<IpcServiceDeps> = {}): IpcServiceDeps {
return { return {
onOverlayModalClosed: () => {}, onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleDevTools: () => {}, toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(), getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false, getPlaybackPaused: () => false,
getSubtitlePosition: () => null, getSubtitlePosition: () => null,
getSubtitleStyle: () => null, getSubtitleStyle: () => null,
saveSubtitlePosition: () => {}, saveSubtitlePosition: () => {},
getMecabStatus: () => ({ available: false, enabled: false, path: null }),
setMecabEnabled: () => {},
handleMpvCommand: () => {}, handleMpvCommand: () => {},
getKeybindings: () => [],
getSessionBindings: () => [], getSessionBindings: () => [],
getConfiguredShortcuts: () => ({}), getConfiguredShortcuts: () => ({}),
dispatchSessionAction: async () => {}, dispatchSessionAction: async () => {},
@@ -139,23 +131,19 @@ function createRegisterIpcDeps(overrides: Partial<IpcServiceDeps> = {}): IpcServ
getOverlayNotificationPosition: () => 'top-right', getOverlayNotificationPosition: () => 'top-right',
getControllerConfig: () => createControllerConfigFixture(), getControllerConfig: () => createControllerConfigFixture(),
saveControllerConfig: async () => {}, saveControllerConfig: async () => {},
saveControllerPreference: async () => {},
getSecondarySubMode: () => 'hover', getSecondarySubMode: () => 'hover',
getCurrentSecondarySub: () => '', getCurrentSecondarySub: () => '',
focusMainWindow: () => {}, focusMainWindow: () => {},
activatePlaybackWindowForOverlayInteraction: () => false, activatePlaybackWindowForOverlayInteraction: () => false,
runSubsyncManual: async () => ({ ok: true, message: 'ok' }), runSubsyncManual: async () => ({ ok: true, message: 'ok' }),
getAnkiConnectStatus: () => false,
getRuntimeOptions: () => [], getRuntimeOptions: () => [],
setRuntimeOption: () => ({ ok: true }), setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {}, reportOverlayContentBounds: () => {},
getAnilistStatus: () => ({}), getAnilistStatus: () => ({}),
clearAnilistToken: () => {}, clearAnilistToken: () => {},
openAnilistSetup: () => {}, openAnilistSetup: () => {},
getAnilistQueueStatus: () => ({}), getAnilistQueueStatus: () => ({}),
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }), retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
getPlaylistBrowserSnapshot: async () => ({ getPlaylistBrowserSnapshot: async () => ({
directoryPath: null, directoryPath: null,
directoryAvailable: false, directoryAvailable: false,
@@ -312,26 +300,17 @@ test('registerIpcHandlers skips AniList update when manual mark watched has no a
test('registerIpcHandlers rejects malformed runtime-option payloads', async () => { test('registerIpcHandlers rejects malformed runtime-option payloads', async () => {
const { registrar, handlers } = createFakeIpcRegistrar(); const { registrar, handlers } = createFakeIpcRegistrar();
const calls: Array<{ id: string; value: unknown }> = []; const calls: Array<{ id: string; value: unknown }> = [];
const cycles: Array<{ id: string; direction: 1 | -1 }> = [];
registerIpcHandlers( registerIpcHandlers(
{ {
onOverlayModalClosed: () => {}, onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleDevTools: () => {}, toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getPlaybackPaused: () => null, getPlaybackPaused: () => null,
getSubtitlePosition: () => null, getSubtitlePosition: () => null,
getSubtitleStyle: () => null, getSubtitleStyle: () => null,
saveSubtitlePosition: () => {}, saveSubtitlePosition: () => {},
getMecabStatus: () => ({ available: false, enabled: false, path: null }),
setMecabEnabled: () => {},
handleMpvCommand: () => {}, handleMpvCommand: () => {},
getKeybindings: () => [],
getSessionBindings: () => [], getSessionBindings: () => [],
getConfiguredShortcuts: () => ({}), getConfiguredShortcuts: () => ({}),
dispatchSessionAction: async () => {}, dispatchSessionAction: async () => {},
@@ -340,21 +319,15 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
getOverlayNotificationPosition: () => 'top-right', getOverlayNotificationPosition: () => 'top-right',
getControllerConfig: () => createControllerConfigFixture(), getControllerConfig: () => createControllerConfigFixture(),
saveControllerConfig: () => {}, saveControllerConfig: () => {},
saveControllerPreference: () => {},
getSecondarySubMode: () => 'hover', getSecondarySubMode: () => 'hover',
getCurrentSecondarySub: () => '', getCurrentSecondarySub: () => '',
focusMainWindow: () => {}, focusMainWindow: () => {},
runSubsyncManual: async () => ({ ok: true, message: 'ok' }), runSubsyncManual: async () => ({ ok: true, message: 'ok' }),
getAnkiConnectStatus: () => false,
getRuntimeOptions: () => [], getRuntimeOptions: () => [],
setRuntimeOption: (id, value) => { setRuntimeOption: (id, value) => {
calls.push({ id, value }); calls.push({ id, value });
return { ok: true }; return { ok: true };
}, },
cycleRuntimeOption: (id, direction) => {
cycles.push({ id, direction });
return { ok: true };
},
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(), getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
reportOverlayContentBounds: () => {}, reportOverlayContentBounds: () => {},
getAnilistStatus: () => ({}), getAnilistStatus: () => ({}),
@@ -362,7 +335,6 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
openAnilistSetup: () => {}, openAnilistSetup: () => {},
getAnilistQueueStatus: () => ({}), getAnilistQueueStatus: () => ({}),
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }), retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
getPlaylistBrowserSnapshot: async () => ({ getPlaylistBrowserSnapshot: async () => ({
directoryPath: null, directoryPath: null,
directoryAvailable: false, directoryAvailable: false,
@@ -399,16 +371,6 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
{ id: 'subtitle.annotation.jlpt', value: false }, { id: 'subtitle.annotation.jlpt', value: false },
]); ]);
const cycleHandler = handlers.handle.get(IPC_CHANNELS.request.cycleRuntimeOption);
assert.ok(cycleHandler);
const invalidDirection = await cycleHandler!({}, 'anki.kikuFieldGrouping', 2);
assert.deepEqual(invalidDirection, {
ok: false,
error: 'Invalid runtime option cycle direction',
});
await cycleHandler!({}, 'anki.kikuFieldGrouping', -1);
assert.deepEqual(cycles, [{ id: 'anki.kikuFieldGrouping', direction: -1 }]);
const getPlaybackPausedHandler = handlers.handle.get(IPC_CHANNELS.request.getPlaybackPaused); const getPlaybackPausedHandler = handlers.handle.get(IPC_CHANNELS.request.getPlaybackPaused);
assert.ok(getPlaybackPausedHandler); assert.ok(getPlaybackPausedHandler);
assert.equal(getPlaybackPausedHandler!({}), null); assert.equal(getPlaybackPausedHandler!({}), null);
@@ -609,7 +571,6 @@ test('registerIpcHandlers records yomitan lookup when subtitle context recording
test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => { test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
const { registrar, handlers } = createFakeIpcRegistrar(); const { registrar, handlers } = createFakeIpcRegistrar();
const saves: unknown[] = []; const saves: unknown[] = [];
const controllerSaves: unknown[] = [];
const closedModals: unknown[] = []; const closedModals: unknown[] = [];
const openedModals: unknown[] = []; const openedModals: unknown[] = [];
registerIpcHandlers( registerIpcHandlers(
@@ -620,14 +581,9 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
onOverlayModalOpened: (modal) => { onOverlayModalOpened: (modal) => {
openedModals.push(modal); openedModals.push(modal);
}, },
openYomitanSettings: () => {},
quitApp: () => {},
toggleDevTools: () => {}, toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(), getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false, getPlaybackPaused: () => false,
getSubtitlePosition: () => null, getSubtitlePosition: () => null,
@@ -635,10 +591,7 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
saveSubtitlePosition: (position) => { saveSubtitlePosition: (position) => {
saves.push(position); saves.push(position);
}, },
getMecabStatus: () => ({ available: false, enabled: false, path: null }),
setMecabEnabled: () => {},
handleMpvCommand: () => {}, handleMpvCommand: () => {},
getKeybindings: () => [],
getSessionBindings: () => [], getSessionBindings: () => [],
getConfiguredShortcuts: () => ({}), getConfiguredShortcuts: () => ({}),
dispatchSessionAction: async () => {}, dispatchSessionAction: async () => {},
@@ -647,24 +600,18 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
getOverlayNotificationPosition: () => 'top-right', getOverlayNotificationPosition: () => 'top-right',
getControllerConfig: () => createControllerConfigFixture(), getControllerConfig: () => createControllerConfigFixture(),
saveControllerConfig: () => {}, saveControllerConfig: () => {},
saveControllerPreference: (update) => {
controllerSaves.push(update);
},
getSecondarySubMode: () => 'hover', getSecondarySubMode: () => 'hover',
getCurrentSecondarySub: () => '', getCurrentSecondarySub: () => '',
focusMainWindow: () => {}, focusMainWindow: () => {},
runSubsyncManual: async () => ({ ok: true, message: 'ok' }), runSubsyncManual: async () => ({ ok: true, message: 'ok' }),
getAnkiConnectStatus: () => false,
getRuntimeOptions: () => [], getRuntimeOptions: () => [],
setRuntimeOption: () => ({ ok: true }), setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {}, reportOverlayContentBounds: () => {},
getAnilistStatus: () => ({}), getAnilistStatus: () => ({}),
clearAnilistToken: () => {}, clearAnilistToken: () => {},
openAnilistSetup: () => {}, openAnilistSetup: () => {},
getAnilistQueueStatus: () => ({}), getAnilistQueueStatus: () => ({}),
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }), retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
getPlaylistBrowserSnapshot: async () => ({ getPlaylistBrowserSnapshot: async () => ({
directoryPath: null, directoryPath: null,
directoryAvailable: false, directoryAvailable: false,
@@ -698,96 +645,6 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
assert.deepEqual(openedModals, ['subsync', 'runtime-options']); assert.deepEqual(openedModals, ['subsync', 'runtime-options']);
}); });
test('registerIpcHandlers awaits saveControllerPreference through request-response IPC', async () => {
const { registrar, handlers } = createFakeIpcRegistrar();
const controllerSaves: unknown[] = [];
registerIpcHandlers(
{
onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
saveSubtitlePosition: () => {},
getMecabStatus: () => ({ available: false, enabled: false, path: null }),
setMecabEnabled: () => {},
handleMpvCommand: () => {},
getKeybindings: () => [],
getSessionBindings: () => [],
getConfiguredShortcuts: () => ({}),
dispatchSessionAction: async () => {},
getStatsToggleKey: () => 'Backquote',
getMarkWatchedKey: () => 'KeyW',
getOverlayNotificationPosition: () => 'top-right',
getControllerConfig: () => createControllerConfigFixture(),
saveControllerConfig: async () => {},
saveControllerPreference: async (update) => {
await Promise.resolve();
controllerSaves.push(update);
},
getSecondarySubMode: () => 'hover',
getCurrentSecondarySub: () => '',
focusMainWindow: () => {},
runSubsyncManual: async () => ({ ok: true, message: 'ok' }),
getAnkiConnectStatus: () => false,
getRuntimeOptions: () => [],
setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {},
getAnilistStatus: () => ({}),
clearAnilistToken: () => {},
openAnilistSetup: () => {},
getAnilistQueueStatus: () => ({}),
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
getPlaylistBrowserSnapshot: async () => ({
directoryPath: null,
directoryAvailable: false,
directoryStatus: '',
directoryItems: [],
playlistItems: [],
playingIndex: null,
currentFilePath: null,
}),
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok' }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
onYoutubePickerResolve: async () => ({ ok: true, message: 'ok' }),
},
registrar,
);
const saveHandler = handlers.handle.get(IPC_CHANNELS.command.saveControllerPreference);
assert.ok(saveHandler);
await assert.rejects(async () => {
await saveHandler!({}, { preferredGamepadId: 12 });
}, /Invalid controller preference payload/);
await saveHandler!(
{},
{
preferredGamepadId: 'pad-1',
preferredGamepadLabel: 'Pad 1',
},
);
assert.deepEqual(controllerSaves, [
{
preferredGamepadId: 'pad-1',
preferredGamepadLabel: 'Pad 1',
},
]);
});
test('registerIpcHandlers accepts per-controller profile config updates', async () => { test('registerIpcHandlers accepts per-controller profile config updates', async () => {
const { registrar, handlers } = createFakeIpcRegistrar(); const { registrar, handlers } = createFakeIpcRegistrar();
const controllerSaves: unknown[] = []; const controllerSaves: unknown[] = [];
@@ -975,76 +832,6 @@ test('registerIpcHandlers forwards valid overlay notification actions', () => {
]); ]);
}); });
test('registerIpcHandlers rejects malformed controller preference payloads', async () => {
const { registrar, handlers } = createFakeIpcRegistrar();
registerIpcHandlers(
{
onOverlayModalClosed: () => {},
openYomitanSettings: () => {},
quitApp: () => {},
toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {},
tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '',
getSubtitleSidebarSnapshot: async () => createSubtitleSidebarSnapshotFixture(),
getPlaybackPaused: () => false,
getSubtitlePosition: () => null,
getSubtitleStyle: () => null,
saveSubtitlePosition: () => {},
getMecabStatus: () => ({ available: false, enabled: false, path: null }),
setMecabEnabled: () => {},
handleMpvCommand: () => {},
getKeybindings: () => [],
getSessionBindings: () => [],
getConfiguredShortcuts: () => ({}),
dispatchSessionAction: async () => {},
getStatsToggleKey: () => 'Backquote',
getMarkWatchedKey: () => 'KeyW',
getOverlayNotificationPosition: () => 'top-right',
getControllerConfig: () => createControllerConfigFixture(),
saveControllerConfig: async () => {},
saveControllerPreference: async () => {},
getSecondarySubMode: () => 'hover',
getCurrentSecondarySub: () => '',
focusMainWindow: () => {},
runSubsyncManual: async () => ({ ok: true, message: 'ok' }),
getAnkiConnectStatus: () => false,
getRuntimeOptions: () => [],
setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {},
getAnilistStatus: () => ({}),
clearAnilistToken: () => {},
openAnilistSetup: () => {},
getAnilistQueueStatus: () => ({}),
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
getPlaylistBrowserSnapshot: async () => ({
directoryPath: null,
directoryAvailable: false,
directoryStatus: '',
directoryItems: [],
playlistItems: [],
playingIndex: null,
currentFilePath: null,
}),
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok' }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
onYoutubePickerResolve: async () => ({ ok: true, message: 'ok' }),
},
registrar,
);
const saveHandler = handlers.handle.get(IPC_CHANNELS.command.saveControllerPreference);
await assert.rejects(async () => {
await saveHandler!({}, { preferredGamepadId: 12 });
}, /Invalid controller preference payload/);
});
test('registerIpcHandlers exposes character dictionary selection handlers', async () => { test('registerIpcHandlers exposes character dictionary selection handlers', async () => {
const { registrar, handlers } = createFakeIpcRegistrar(); const { registrar, handlers } = createFakeIpcRegistrar();
const calls: number[] = []; const calls: number[] = [];
-124
View File
@@ -7,7 +7,6 @@ import type {
ControllerConfigUpdate, ControllerConfigUpdate,
PlaylistBrowserMutationResult, PlaylistBrowserMutationResult,
PlaylistBrowserSnapshot, PlaylistBrowserSnapshot,
ControllerPreferenceUpdate,
ResolvedControllerConfig, ResolvedControllerConfig,
RuntimeOptionId, RuntimeOptionId,
RuntimeOptionValue, RuntimeOptionValue,
@@ -34,10 +33,8 @@ import { IPC_CHANNELS, type OverlayHostedModal } from '../../shared/ipc/contract
import { import {
parseMpvCommand, parseMpvCommand,
parseControllerConfigUpdate, parseControllerConfigUpdate,
parseControllerPreferenceUpdate,
parseOptionalForwardingOptions, parseOptionalForwardingOptions,
parseOverlayHostedModal, parseOverlayHostedModal,
parseRuntimeOptionDirection,
parseRuntimeOptionId, parseRuntimeOptionId,
parseRuntimeOptionValue, parseRuntimeOptionValue,
parseSessionActionDispatchRequest, parseSessionActionDispatchRequest,
@@ -71,14 +68,9 @@ export interface IpcServiceDeps {
actionId: string, actionId: string,
noteId?: number, noteId?: number,
) => void | Promise<void>; ) => void | Promise<void>;
openYomitanSettings: () => void;
quitApp: () => void;
toggleDevTools: () => void; toggleDevTools: () => void;
getVisibleOverlayVisibility: () => boolean;
toggleVisibleOverlay: () => void;
tokenizeCurrentSubtitle: () => Promise<unknown>; tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string; getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string;
getSubtitleSidebarSnapshot?: () => Promise<SubtitleSidebarSnapshot>; getSubtitleSidebarSnapshot?: () => Promise<SubtitleSidebarSnapshot>;
getSubtitleSidebarPlaybackState?: () => SubtitleSidebarPlaybackState; getSubtitleSidebarPlaybackState?: () => SubtitleSidebarPlaybackState;
getSubtitleSidebarOpen?: () => boolean; getSubtitleSidebarOpen?: () => boolean;
@@ -86,14 +78,7 @@ export interface IpcServiceDeps {
getSubtitlePosition: () => unknown; getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown; getSubtitleStyle: () => unknown;
saveSubtitlePosition: (position: SubtitlePosition) => void; saveSubtitlePosition: (position: SubtitlePosition) => void;
getMecabStatus: () => {
available: boolean;
enabled: boolean;
path: string | null;
};
setMecabEnabled: (enabled: boolean) => void;
handleMpvCommand: (command: Array<string | number>) => void; handleMpvCommand: (command: Array<string | number>) => void;
getKeybindings: () => unknown;
getMpvInputBindings?: () => Promise<MpvInputBindingsSnapshot>; getMpvInputBindings?: () => Promise<MpvInputBindingsSnapshot>;
getSessionBindings?: () => CompiledSessionBinding[] | Promise<CompiledSessionBinding[]>; getSessionBindings?: () => CompiledSessionBinding[] | Promise<CompiledSessionBinding[]>;
getConfiguredShortcuts: () => unknown; getConfiguredShortcuts: () => unknown;
@@ -103,7 +88,6 @@ export interface IpcServiceDeps {
getOverlayNotificationPosition: () => string; getOverlayNotificationPosition: () => string;
getControllerConfig: () => ResolvedControllerConfig; getControllerConfig: () => ResolvedControllerConfig;
saveControllerConfig: (update: ControllerConfigUpdate) => void | Promise<void>; saveControllerConfig: (update: ControllerConfigUpdate) => void | Promise<void>;
saveControllerPreference: (update: ControllerPreferenceUpdate) => void | Promise<void>;
getSecondarySubMode: () => unknown; getSecondarySubMode: () => unknown;
getCurrentSecondarySub: () => string; getCurrentSecondarySub: () => string;
focusMainWindow: () => void; focusMainWindow: () => void;
@@ -125,10 +109,8 @@ export interface IpcServiceDeps {
resolveMediaTimingReview?: ( resolveMediaTimingReview?: (
request: MediaTimingReviewResolveRequest, request: MediaTimingReviewResolveRequest,
) => MediaTimingReviewActionResult | Promise<MediaTimingReviewActionResult>; ) => MediaTimingReviewActionResult | Promise<MediaTimingReviewActionResult>;
getAnkiConnectStatus: () => boolean;
getRuntimeOptions: () => unknown; getRuntimeOptions: () => unknown;
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown;
reportOverlayContentBounds: ( reportOverlayContentBounds: (
payload: unknown, payload: unknown,
senderWindow: ElectronBrowserWindow | null, senderWindow: ElectronBrowserWindow | null,
@@ -149,7 +131,6 @@ export interface IpcServiceDeps {
getCharacterDictionaryManagerSnapshot?: () => Promise<unknown>; getCharacterDictionaryManagerSnapshot?: () => Promise<unknown>;
removeCharacterDictionaryManagedEntry?: (mediaId: number) => Promise<unknown>; removeCharacterDictionaryManagedEntry?: (mediaId: number) => Promise<unknown>;
moveCharacterDictionaryManagedEntry?: (mediaId: number, direction: 1 | -1) => Promise<unknown>; moveCharacterDictionaryManagedEntry?: (mediaId: number, direction: 1 | -1) => Promise<unknown>;
appendClipboardVideoToQueue: () => { ok: boolean; message: string };
getChangelogSnapshot?: (options?: { refresh?: boolean }) => Promise<ChangelogSnapshot>; getChangelogSnapshot?: (options?: { refresh?: boolean }) => Promise<ChangelogSnapshot>;
getPlaylistBrowserSnapshot: () => Promise<PlaylistBrowserSnapshot>; getPlaylistBrowserSnapshot: () => Promise<PlaylistBrowserSnapshot>;
appendPlaylistBrowserFile: (filePath: string) => Promise<PlaylistBrowserMutationResult>; appendPlaylistBrowserFile: (filePath: string) => Promise<PlaylistBrowserMutationResult>;
@@ -174,15 +155,6 @@ interface WindowLike {
}; };
} }
interface MecabTokenizerLike {
getStatus: () => {
available: boolean;
enabled: boolean;
path: string | null;
};
setEnabled: (enabled: boolean) => void;
}
interface MpvClientLike { interface MpvClientLike {
currentSecondarySubText?: string; currentSecondarySubText?: string;
} }
@@ -342,7 +314,6 @@ function parseMediaTimingReviewResolveRequest(
export interface IpcDepsRuntimeOptions { export interface IpcDepsRuntimeOptions {
getMainWindow: () => WindowLike | null; getMainWindow: () => WindowLike | null;
getVisibleOverlayVisibility: () => boolean;
onOverlayModalClosed: ( onOverlayModalClosed: (
modal: OverlayHostedModal, modal: OverlayHostedModal,
senderWindow: ElectronBrowserWindow | null, senderWindow: ElectronBrowserWindow | null,
@@ -364,12 +335,8 @@ export interface IpcDepsRuntimeOptions {
actionId: string, actionId: string,
noteId?: number, noteId?: number,
) => void | Promise<void>; ) => void | Promise<void>;
openYomitanSettings: () => void;
quitApp: () => void;
toggleVisibleOverlay: () => void;
tokenizeCurrentSubtitle: () => Promise<unknown>; tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string; getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string;
getSubtitleSidebarSnapshot?: () => Promise<SubtitleSidebarSnapshot>; getSubtitleSidebarSnapshot?: () => Promise<SubtitleSidebarSnapshot>;
getSubtitleSidebarPlaybackState?: () => SubtitleSidebarPlaybackState; getSubtitleSidebarPlaybackState?: () => SubtitleSidebarPlaybackState;
getSubtitleSidebarOpen?: () => boolean; getSubtitleSidebarOpen?: () => boolean;
@@ -377,9 +344,7 @@ export interface IpcDepsRuntimeOptions {
getSubtitlePosition: () => unknown; getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown; getSubtitleStyle: () => unknown;
saveSubtitlePosition: (position: SubtitlePosition) => void; saveSubtitlePosition: (position: SubtitlePosition) => void;
getMecabTokenizer: () => MecabTokenizerLike | null;
handleMpvCommand: (command: Array<string | number>) => void; handleMpvCommand: (command: Array<string | number>) => void;
getKeybindings: () => unknown;
getMpvInputBindings?: () => Promise<MpvInputBindingsSnapshot>; getMpvInputBindings?: () => Promise<MpvInputBindingsSnapshot>;
getSessionBindings?: () => CompiledSessionBinding[] | Promise<CompiledSessionBinding[]>; getSessionBindings?: () => CompiledSessionBinding[] | Promise<CompiledSessionBinding[]>;
getConfiguredShortcuts: () => unknown; getConfiguredShortcuts: () => unknown;
@@ -389,7 +354,6 @@ export interface IpcDepsRuntimeOptions {
getOverlayNotificationPosition: () => string; getOverlayNotificationPosition: () => string;
getControllerConfig: () => ResolvedControllerConfig; getControllerConfig: () => ResolvedControllerConfig;
saveControllerConfig: (update: ControllerConfigUpdate) => void | Promise<void>; saveControllerConfig: (update: ControllerConfigUpdate) => void | Promise<void>;
saveControllerPreference: (update: ControllerPreferenceUpdate) => void | Promise<void>;
getSecondarySubMode: () => unknown; getSecondarySubMode: () => unknown;
getMpvClient: () => MpvClientLike | null; getMpvClient: () => MpvClientLike | null;
focusMainWindow: () => void; focusMainWindow: () => void;
@@ -403,10 +367,8 @@ export interface IpcDepsRuntimeOptions {
getMediaTimingReviewWaveform?: IpcServiceDeps['getMediaTimingReviewWaveform']; getMediaTimingReviewWaveform?: IpcServiceDeps['getMediaTimingReviewWaveform'];
stopMediaTimingReviewPreview?: IpcServiceDeps['stopMediaTimingReviewPreview']; stopMediaTimingReviewPreview?: IpcServiceDeps['stopMediaTimingReviewPreview'];
resolveMediaTimingReview?: IpcServiceDeps['resolveMediaTimingReview']; resolveMediaTimingReview?: IpcServiceDeps['resolveMediaTimingReview'];
getAnkiConnectStatus: () => boolean;
getRuntimeOptions: () => unknown; getRuntimeOptions: () => unknown;
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown;
reportOverlayContentBounds: (payload: unknown) => void; reportOverlayContentBounds: (payload: unknown) => void;
getAnilistStatus: () => unknown; getAnilistStatus: () => unknown;
clearAnilistToken: () => void; clearAnilistToken: () => void;
@@ -424,7 +386,6 @@ export interface IpcDepsRuntimeOptions {
getCharacterDictionaryManagerSnapshot?: () => Promise<unknown>; getCharacterDictionaryManagerSnapshot?: () => Promise<unknown>;
removeCharacterDictionaryManagedEntry?: (mediaId: number) => Promise<unknown>; removeCharacterDictionaryManagedEntry?: (mediaId: number) => Promise<unknown>;
moveCharacterDictionaryManagedEntry?: (mediaId: number, direction: 1 | -1) => Promise<unknown>; moveCharacterDictionaryManagedEntry?: (mediaId: number, direction: 1 | -1) => Promise<unknown>;
appendClipboardVideoToQueue: () => { ok: boolean; message: string };
getChangelogSnapshot?: (options?: { refresh?: boolean }) => Promise<ChangelogSnapshot>; getChangelogSnapshot?: (options?: { refresh?: boolean }) => Promise<ChangelogSnapshot>;
getPlaylistBrowserSnapshot: () => Promise<PlaylistBrowserSnapshot>; getPlaylistBrowserSnapshot: () => Promise<PlaylistBrowserSnapshot>;
appendPlaylistBrowserFile: (filePath: string) => Promise<PlaylistBrowserMutationResult>; appendPlaylistBrowserFile: (filePath: string) => Promise<PlaylistBrowserMutationResult>;
@@ -444,19 +405,14 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
onOverlayMouseInteractionChanged: options.onOverlayMouseInteractionChanged, onOverlayMouseInteractionChanged: options.onOverlayMouseInteractionChanged,
onOverlayInteractiveHint: options.onOverlayInteractiveHint, onOverlayInteractiveHint: options.onOverlayInteractiveHint,
handleOverlayNotificationAction: options.handleOverlayNotificationAction, handleOverlayNotificationAction: options.handleOverlayNotificationAction,
openYomitanSettings: options.openYomitanSettings,
recordSubtitleMiningContext: options.recordSubtitleMiningContext, recordSubtitleMiningContext: options.recordSubtitleMiningContext,
quitApp: options.quitApp,
toggleDevTools: () => { toggleDevTools: () => {
const mainWindow = options.getMainWindow(); const mainWindow = options.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) return; if (!mainWindow || mainWindow.isDestroyed()) return;
mainWindow.webContents.toggleDevTools(); mainWindow.webContents.toggleDevTools();
}, },
getVisibleOverlayVisibility: options.getVisibleOverlayVisibility,
toggleVisibleOverlay: options.toggleVisibleOverlay,
tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle, tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle,
getCurrentSubtitleRaw: options.getCurrentSubtitleRaw, getCurrentSubtitleRaw: options.getCurrentSubtitleRaw,
getCurrentSubtitleAss: options.getCurrentSubtitleAss,
getSubtitleSidebarSnapshot: options.getSubtitleSidebarSnapshot, getSubtitleSidebarSnapshot: options.getSubtitleSidebarSnapshot,
getSubtitleSidebarPlaybackState: options.getSubtitleSidebarPlaybackState, getSubtitleSidebarPlaybackState: options.getSubtitleSidebarPlaybackState,
getSubtitleSidebarOpen: options.getSubtitleSidebarOpen ?? (() => false), getSubtitleSidebarOpen: options.getSubtitleSidebarOpen ?? (() => false),
@@ -464,19 +420,7 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
getSubtitlePosition: options.getSubtitlePosition, getSubtitlePosition: options.getSubtitlePosition,
getSubtitleStyle: options.getSubtitleStyle, getSubtitleStyle: options.getSubtitleStyle,
saveSubtitlePosition: options.saveSubtitlePosition, saveSubtitlePosition: options.saveSubtitlePosition,
getMecabStatus: () => {
const mecabTokenizer = options.getMecabTokenizer();
return mecabTokenizer
? mecabTokenizer.getStatus()
: { available: false, enabled: false, path: null };
},
setMecabEnabled: (enabled) => {
const mecabTokenizer = options.getMecabTokenizer();
if (!mecabTokenizer) return;
mecabTokenizer.setEnabled(enabled);
},
handleMpvCommand: options.handleMpvCommand, handleMpvCommand: options.handleMpvCommand,
getKeybindings: options.getKeybindings,
getMpvInputBindings: options.getMpvInputBindings, getMpvInputBindings: options.getMpvInputBindings,
getSessionBindings: options.getSessionBindings ?? (() => []), getSessionBindings: options.getSessionBindings ?? (() => []),
getConfiguredShortcuts: options.getConfiguredShortcuts, getConfiguredShortcuts: options.getConfiguredShortcuts,
@@ -486,7 +430,6 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
getOverlayNotificationPosition: options.getOverlayNotificationPosition, getOverlayNotificationPosition: options.getOverlayNotificationPosition,
getControllerConfig: options.getControllerConfig, getControllerConfig: options.getControllerConfig,
saveControllerConfig: options.saveControllerConfig, saveControllerConfig: options.saveControllerConfig,
saveControllerPreference: options.saveControllerPreference,
getSecondarySubMode: options.getSecondarySubMode, getSecondarySubMode: options.getSecondarySubMode,
getCurrentSecondarySub: () => options.getMpvClient()?.currentSecondarySubText || '', getCurrentSecondarySub: () => options.getMpvClient()?.currentSecondarySubText || '',
focusMainWindow: () => { focusMainWindow: () => {
@@ -503,10 +446,8 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
getMediaTimingReviewWaveform: options.getMediaTimingReviewWaveform, getMediaTimingReviewWaveform: options.getMediaTimingReviewWaveform,
stopMediaTimingReviewPreview: options.stopMediaTimingReviewPreview, stopMediaTimingReviewPreview: options.stopMediaTimingReviewPreview,
resolveMediaTimingReview: options.resolveMediaTimingReview, resolveMediaTimingReview: options.resolveMediaTimingReview,
getAnkiConnectStatus: options.getAnkiConnectStatus,
getRuntimeOptions: options.getRuntimeOptions, getRuntimeOptions: options.getRuntimeOptions,
setRuntimeOption: options.setRuntimeOption, setRuntimeOption: options.setRuntimeOption,
cycleRuntimeOption: options.cycleRuntimeOption,
reportOverlayContentBounds: (payload, senderWindow) => { reportOverlayContentBounds: (payload, senderWindow) => {
const mainWindow = options.getMainWindow(); const mainWindow = options.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) return; if (!mainWindow || mainWindow.isDestroyed()) return;
@@ -553,7 +494,6 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
message: 'Character dictionary manager unavailable.', message: 'Character dictionary manager unavailable.',
entries: [], entries: [],
})), })),
appendClipboardVideoToQueue: options.appendClipboardVideoToQueue,
getChangelogSnapshot: options.getChangelogSnapshot, getChangelogSnapshot: options.getChangelogSnapshot,
getPlaylistBrowserSnapshot: options.getPlaylistBrowserSnapshot, getPlaylistBrowserSnapshot: options.getPlaylistBrowserSnapshot,
appendPlaylistBrowserFile: options.appendPlaylistBrowserFile, appendPlaylistBrowserFile: options.appendPlaylistBrowserFile,
@@ -680,10 +620,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
}, },
); );
ipc.on(IPC_CHANNELS.command.openYomitanSettings, () => {
deps.openYomitanSettings();
});
ipc.on(IPC_CHANNELS.command.recordYomitanLookup, (_event: unknown, payload: unknown) => { ipc.on(IPC_CHANNELS.command.recordYomitanLookup, (_event: unknown, payload: unknown) => {
try { try {
deps.recordSubtitleMiningContext?.(parseSubtitleMiningContext(payload)); deps.recordSubtitleMiningContext?.(parseSubtitleMiningContext(payload));
@@ -711,22 +647,10 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return marked; return marked;
}); });
ipc.on(IPC_CHANNELS.command.quitApp, () => {
deps.quitApp();
});
ipc.on(IPC_CHANNELS.command.toggleDevTools, () => { ipc.on(IPC_CHANNELS.command.toggleDevTools, () => {
deps.toggleDevTools(); deps.toggleDevTools();
}); });
ipc.on(IPC_CHANNELS.command.toggleOverlay, () => {
deps.toggleVisibleOverlay();
});
ipc.handle(IPC_CHANNELS.request.getVisibleOverlayVisibility, () => {
return deps.getVisibleOverlayVisibility();
});
ipc.handle(IPC_CHANNELS.request.getCurrentSubtitle, async () => { ipc.handle(IPC_CHANNELS.request.getCurrentSubtitle, async () => {
return await deps.tokenizeCurrentSubtitle(); return await deps.tokenizeCurrentSubtitle();
}); });
@@ -735,10 +659,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return deps.getCurrentSubtitleRaw(); return deps.getCurrentSubtitleRaw();
}); });
ipc.handle(IPC_CHANNELS.request.getCurrentSubtitleAss, () => {
return deps.getCurrentSubtitleAss();
});
ipc.handle(IPC_CHANNELS.request.getSubtitleSidebarSnapshot, async () => { ipc.handle(IPC_CHANNELS.request.getSubtitleSidebarSnapshot, async () => {
if (!deps.getSubtitleSidebarSnapshot) { if (!deps.getSubtitleSidebarSnapshot) {
throw new Error('Subtitle sidebar snapshot is unavailable.'); throw new Error('Subtitle sidebar snapshot is unavailable.');
@@ -775,17 +695,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
deps.saveSubtitlePosition(parsedPosition); deps.saveSubtitlePosition(parsedPosition);
}); });
ipc.handle(
IPC_CHANNELS.command.saveControllerPreference,
async (_event: unknown, update: unknown) => {
const parsedUpdate = parseControllerPreferenceUpdate(update);
if (!parsedUpdate) {
throw new Error('Invalid controller preference payload');
}
await deps.saveControllerPreference(parsedUpdate);
},
);
ipc.handle( ipc.handle(
IPC_CHANNELS.command.saveControllerConfig, IPC_CHANNELS.command.saveControllerConfig,
async (_event: unknown, update: unknown) => { async (_event: unknown, update: unknown) => {
@@ -797,15 +706,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
}, },
); );
ipc.handle(IPC_CHANNELS.request.getMecabStatus, () => {
return deps.getMecabStatus();
});
ipc.on(IPC_CHANNELS.command.setMecabEnabled, (_event: unknown, enabled: unknown) => {
if (typeof enabled !== 'boolean') return;
deps.setMecabEnabled(enabled);
});
ipc.on(IPC_CHANNELS.command.mpvCommand, (_event: unknown, command: unknown) => { ipc.on(IPC_CHANNELS.command.mpvCommand, (_event: unknown, command: unknown) => {
const parsedCommand = parseMpvCommand(command); const parsedCommand = parseMpvCommand(command);
if (!parsedCommand) return; if (!parsedCommand) return;
@@ -823,10 +723,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
}, },
); );
ipc.handle(IPC_CHANNELS.request.getKeybindings, () => {
return deps.getKeybindings();
});
ipc.handle(IPC_CHANNELS.request.getMpvInputBindings, () => { ipc.handle(IPC_CHANNELS.request.getMpvInputBindings, () => {
return deps.getMpvInputBindings?.() ?? { keys: [], blockedKeys: [] }; return deps.getMpvInputBindings?.() ?? { keys: [], blockedKeys: [] };
}); });
@@ -879,10 +775,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return await deps.runSubsyncManual(parsedRequest); return await deps.runSubsyncManual(parsedRequest);
}); });
ipc.handle(IPC_CHANNELS.request.getAnkiConnectStatus, () => {
return deps.getAnkiConnectStatus();
});
ipc.handle(IPC_CHANNELS.request.getRuntimeOptions, () => { ipc.handle(IPC_CHANNELS.request.getRuntimeOptions, () => {
return deps.getRuntimeOptions(); return deps.getRuntimeOptions();
}); });
@@ -899,18 +791,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return deps.setRuntimeOption(parsedId, parsedValue); return deps.setRuntimeOption(parsedId, parsedValue);
}); });
ipc.handle(IPC_CHANNELS.request.cycleRuntimeOption, (_event, id: unknown, direction: unknown) => {
const parsedId = parseRuntimeOptionId(id);
if (!parsedId) {
return { ok: false, error: 'Invalid runtime option id' };
}
const parsedDirection = parseRuntimeOptionDirection(direction);
if (!parsedDirection) {
return { ok: false, error: 'Invalid runtime option cycle direction' };
}
return deps.cycleRuntimeOption(parsedId, parsedDirection);
});
ipc.on(IPC_CHANNELS.command.reportOverlayContentBounds, (event: unknown, payload: unknown) => { ipc.on(IPC_CHANNELS.command.reportOverlayContentBounds, (event: unknown, payload: unknown) => {
const senderWindow = const senderWindow =
electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null; electron.BrowserWindow?.fromWebContents((event as IpcMainEvent).sender) ?? null;
@@ -1020,10 +900,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
}, },
); );
ipc.handle(IPC_CHANNELS.request.appendClipboardVideoToQueue, () => {
return deps.appendClipboardVideoToQueue();
});
ipc.handle(IPC_CHANNELS.request.getChangelogSnapshot, async (_event, payload: unknown) => { ipc.handle(IPC_CHANNELS.request.getChangelogSnapshot, async (_event, payload: unknown) => {
const refresh = const refresh =
typeof payload === 'object' && payload !== null && 'refresh' in payload typeof payload === 'object' && payload !== null && 'refresh' in payload
-6
View File
@@ -20,7 +20,6 @@ import {
MPV_REQUEST_ID_SUB_SHADOW_OFFSET, MPV_REQUEST_ID_SUB_SHADOW_OFFSET,
MPV_REQUEST_ID_SUB_SPACING, MPV_REQUEST_ID_SUB_SPACING,
MPV_REQUEST_ID_SUBTEXT, MPV_REQUEST_ID_SUBTEXT,
MPV_REQUEST_ID_SUBTEXT_ASS,
MPV_REQUEST_ID_SUB_USE_MARGINS, MPV_REQUEST_ID_SUB_USE_MARGINS,
MPV_REQUEST_ID_PAUSE, MPV_REQUEST_ID_PAUSE,
} from './mpv-protocol'; } from './mpv-protocol';
@@ -54,7 +53,6 @@ const MPV_SUBTITLE_PROPERTY_OBSERVATIONS: string[] = [
'sub-scale-by-window', 'sub-scale-by-window',
'osd-height', 'osd-height',
'osd-dimensions', 'osd-dimensions',
'sub-text/ass',
'sub-border-size', 'sub-border-size',
'sub-shadow-offset', 'sub-shadow-offset',
'sub-ass-override', 'sub-ass-override',
@@ -76,10 +74,6 @@ const MPV_INITIAL_PROPERTY_REQUESTS: Array<MpvProtocolCommand> = [
command: ['get_property', 'sub-text'], command: ['get_property', 'sub-text'],
request_id: MPV_REQUEST_ID_SUBTEXT, request_id: MPV_REQUEST_ID_SUBTEXT,
}, },
{
command: ['get_property', 'sub-text/ass'],
request_id: MPV_REQUEST_ID_SUBTEXT_ASS,
},
{ {
command: ['get_property', 'path'], command: ['get_property', 'path'],
request_id: MPV_REQUEST_ID_PATH, request_id: MPV_REQUEST_ID_PATH,
-23
View File
@@ -59,7 +59,6 @@ function createDeps(overrides: Partial<MpvProtocolHandleMessageDeps> = {}): {
getSubtitleMetrics: () => metrics, getSubtitleMetrics: () => metrics,
isVisibleOverlayVisible: () => false, isVisibleOverlayVisible: () => false,
emitSubtitleChange: (payload) => state.events.push(payload), emitSubtitleChange: (payload) => state.events.push(payload),
emitSubtitleAssChange: (payload) => state.events.push(payload),
emitSubtitleTiming: (payload) => state.events.push(payload), emitSubtitleTiming: (payload) => state.events.push(payload),
emitSecondarySubtitleChange: (payload) => state.events.push(payload), emitSecondarySubtitleChange: (payload) => state.events.push(payload),
emitSubtitleTrackChange: (payload) => state.events.push(payload), emitSubtitleTrackChange: (payload) => state.events.push(payload),
@@ -132,28 +131,6 @@ test('dispatchMpvProtocolMessage emits subtitle text on property change', async
assert.deepEqual(state.events, [{ text: '字幕', isOverlayVisible: false }]); assert.deepEqual(state.events, [{ text: '字幕', isOverlayVisible: false }]);
}); });
test('dispatchMpvProtocolMessage emits ASS subtitle text from the current mpv property', async () => {
const { deps, state } = createDeps();
await dispatchMpvProtocolMessage(
{ event: 'property-change', name: 'sub-text/ass', data: '{\\b1}字幕' },
deps,
);
assert.deepEqual(state.events, [{ text: '{\\b1}字幕' }]);
});
test('dispatchMpvProtocolMessage emits ASS subtitle text from the legacy mpv property', async () => {
const { deps, state } = createDeps();
await dispatchMpvProtocolMessage(
{ event: 'property-change', name: 'sub-text-ass', data: '{\\b1}字幕' },
deps,
);
assert.deepEqual(state.events, [{ text: '{\\b1}字幕' }]);
});
test('dispatchMpvProtocolMessage emits subtitle track changes', async () => { test('dispatchMpvProtocolMessage emits subtitle track changes', async () => {
const { deps, state } = createDeps({ const { deps, state } = createDeps({
emitSubtitleTrackChange: (payload) => state.events.push(payload), emitSubtitleTrackChange: (payload) => state.events.push(payload),
-6
View File
@@ -24,7 +24,6 @@ export const MPV_REQUEST_ID_SUB_FONT = 111;
export const MPV_REQUEST_ID_SUB_SCALE_BY_WINDOW = 112; export const MPV_REQUEST_ID_SUB_SCALE_BY_WINDOW = 112;
export const MPV_REQUEST_ID_OSD_HEIGHT = 113; export const MPV_REQUEST_ID_OSD_HEIGHT = 113;
export const MPV_REQUEST_ID_OSD_DIMENSIONS = 114; export const MPV_REQUEST_ID_OSD_DIMENSIONS = 114;
export const MPV_REQUEST_ID_SUBTEXT_ASS = 115;
export const MPV_REQUEST_ID_SUB_SPACING = 116; export const MPV_REQUEST_ID_SUB_SPACING = 116;
export const MPV_REQUEST_ID_SUB_BOLD = 117; export const MPV_REQUEST_ID_SUB_BOLD = 117;
export const MPV_REQUEST_ID_SUB_ITALIC = 118; export const MPV_REQUEST_ID_SUB_ITALIC = 118;
@@ -52,7 +51,6 @@ export interface MpvProtocolHandleMessageDeps {
getSubtitleMetrics: () => MpvSubtitleRenderMetrics; getSubtitleMetrics: () => MpvSubtitleRenderMetrics;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
emitSubtitleChange: (payload: { text: string; isOverlayVisible: boolean }) => void; emitSubtitleChange: (payload: { text: string; isOverlayVisible: boolean }) => void;
emitSubtitleAssChange: (payload: { text: string }) => void;
emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void; emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void;
emitSecondarySubtitleChange: (payload: { text: string }) => void; emitSecondarySubtitleChange: (payload: { text: string }) => void;
emitSubtitleTrackChange: (payload: { sid: number | null }) => void; emitSubtitleTrackChange: (payload: { sid: number | null }) => void;
@@ -253,8 +251,6 @@ export async function dispatchMpvProtocolMessage(
isOverlayVisible: overlayVisible, isOverlayVisible: overlayVisible,
}); });
deps.setCurrentSubText(nextSubText); deps.setCurrentSubText(nextSubText);
} else if (msg.name === 'sub-text/ass' || msg.name === 'sub-text-ass') {
deps.emitSubtitleAssChange({ text: (msg.data as string) || '' });
} else if (msg.name === 'sub-start') { } else if (msg.name === 'sub-start') {
deps.setCurrentSubStart((msg.data as number) || 0); deps.setCurrentSubStart((msg.data as number) || 0);
deps.emitSubtitleTiming({ deps.emitSubtitleTiming({
@@ -468,8 +464,6 @@ export async function dispatchMpvProtocolMessage(
text: nextSubText, text: nextSubText,
isOverlayVisible: deps.isVisibleOverlayVisible(), isOverlayVisible: deps.isVisibleOverlayVisible(),
}); });
} else if (msg.request_id === MPV_REQUEST_ID_SUBTEXT_ASS) {
deps.emitSubtitleAssChange({ text: (msg.data as string) || '' });
} else if (msg.request_id === MPV_REQUEST_ID_PATH) { } else if (msg.request_id === MPV_REQUEST_ID_PATH) {
deps.emitMediaPathChange({ path: (msg.data as string) || '' }); deps.emitMediaPathChange({ path: (msg.data as string) || '' });
} else if (msg.request_id === MPV_REQUEST_ID_MEDIA_TITLE) { } else if (msg.request_id === MPV_REQUEST_ID_MEDIA_TITLE) {
-13
View File
@@ -444,17 +444,6 @@ test('MpvIpcClient reconnect replays property subscriptions and initial state re
(command as { command: unknown[] }).command[1] === 1 && (command as { command: unknown[] }).command[1] === 1 &&
(command as { command: unknown[] }).command[2] === 'sub-text', (command as { command: unknown[] }).command[2] === 'sub-text',
); );
const hasAssSubtitleSubscription = commands.some(
(command) =>
Array.isArray((command as { command: unknown[] }).command) &&
(command as { command: unknown[] }).command[0] === 'observe_property' &&
(command as { command: unknown[] }).command[2] === 'sub-text/ass',
);
const hasDeprecatedAssSubtitleProperty = commands.some(
(command) =>
Array.isArray((command as { command: unknown[] }).command) &&
(command as { command: unknown[] }).command.includes('sub-text-ass'),
);
const hasPathRequest = commands.some( const hasPathRequest = commands.some(
(command) => (command) =>
Array.isArray((command as { command: unknown[] }).command) && Array.isArray((command as { command: unknown[] }).command) &&
@@ -464,8 +453,6 @@ test('MpvIpcClient reconnect replays property subscriptions and initial state re
assert.equal(hasSecondaryVisibilityReset, true); assert.equal(hasSecondaryVisibilityReset, true);
assert.equal(hasTrackSubscription, true); assert.equal(hasTrackSubscription, true);
assert.equal(hasAssSubtitleSubscription, true);
assert.equal(hasDeprecatedAssSubtitleProperty, false);
assert.equal(hasPathRequest, true); assert.equal(hasPathRequest, true);
}); });
-4
View File
@@ -123,7 +123,6 @@ export interface MpvIpcClientDeps extends MpvIpcClientProtocolDeps {}
export interface MpvIpcClientEventMap { export interface MpvIpcClientEventMap {
'connection-change': { connected: boolean }; 'connection-change': { connected: boolean };
'subtitle-change': { text: string; isOverlayVisible: boolean }; 'subtitle-change': { text: string; isOverlayVisible: boolean };
'subtitle-ass-change': { text: string };
'subtitle-timing': { text: string; start: number; end: number }; 'subtitle-timing': { text: string; start: number; end: number };
'time-pos-change': { time: number }; 'time-pos-change': { time: number };
'duration-change': { duration: number }; 'duration-change': { duration: number };
@@ -417,9 +416,6 @@ export class MpvIpcClient implements MpvClient {
emitSubtitleChange: (payload) => { emitSubtitleChange: (payload) => {
this.emit('subtitle-change', payload); this.emit('subtitle-change', payload);
}, },
emitSubtitleAssChange: (payload) => {
this.emit('subtitle-ass-change', payload);
},
emitSubtitleTiming: (payload) => { emitSubtitleTiming: (payload) => {
this.emit('subtitle-timing', payload); this.emit('subtitle-timing', payload);
}, },
-30
View File
@@ -74,9 +74,6 @@ export function updateVisibleOverlayVisibility(args: {
overlayInteractionActive?: boolean; overlayInteractionActive?: boolean;
mainWindow: BrowserWindow | null; mainWindow: BrowserWindow | null;
windowTracker: BaseWindowTracker | null; windowTracker: BaseWindowTracker | null;
lastKnownWindowsForegroundProcessName?: string | null;
windowsOverlayProcessName?: string | null;
windowsFocusHandoffGraceActive?: boolean;
macOSForegroundProbeActive?: boolean; macOSForegroundProbeActive?: boolean;
trackerNotReadyWarningShown: boolean; trackerNotReadyWarningShown: boolean;
setTrackerNotReadyWarningShown: (shown: boolean) => void; setTrackerNotReadyWarningShown: (shown: boolean) => void;
@@ -168,26 +165,6 @@ export function updateVisibleOverlayVisibility(args: {
const shouldUseMacOSMousePassthrough = args.isMacOSPlatform && !overlayInteractionActive; const shouldUseMacOSMousePassthrough = args.isMacOSPlatform && !overlayInteractionActive;
const shouldDefaultToPassthrough = const shouldDefaultToPassthrough =
args.isWindowsPlatform || forceMousePassthrough || shouldReleaseMacOSOverlayLevel; 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 shouldForcePassiveReshow = args.isWindowsPlatform && !wasVisible;
const isNonNativeOverlay = !args.isWindowsPlatform && !args.isMacOSPlatform; const isNonNativeOverlay = !args.isWindowsPlatform && !args.isMacOSPlatform;
const isNonNativePassiveOverlay = isNonNativeOverlay && !overlayInteractionActive; const isNonNativePassiveOverlay = isNonNativeOverlay && !overlayInteractionActive;
@@ -208,13 +185,6 @@ export function updateVisibleOverlayVisibility(args: {
(isNonNativePassiveOverlay && !hasNonNativeInputRegion) || (isNonNativePassiveOverlay && !hasNonNativeInputRegion) ||
(shouldDefaultToPassthrough && (!isVisibleOverlayFocused || shouldForcePassiveReshow)); (shouldDefaultToPassthrough && (!isVisibleOverlayFocused || shouldForcePassiveReshow));
const shouldBindTrackedWindowsOverlay = args.isWindowsPlatform && !!args.windowTracker; const shouldBindTrackedWindowsOverlay = args.isWindowsPlatform && !!args.windowTracker;
const shouldKeepTrackedWindowsOverlayTopmost =
!args.isWindowsPlatform ||
!args.windowTracker ||
isVisibleOverlayFocused ||
isTrackedWindowsTargetFocused ||
shouldPreserveWindowsOverlayDuringFocusHandoff ||
(hasWindowsForegroundProcessSignal && windowsForegroundProcessName === 'mpv');
if (shouldIgnoreMouseEvents) { if (shouldIgnoreMouseEvents) {
applyOverlayClickThrough(mainWindow, args.isWindowsPlatform); applyOverlayClickThrough(mainWindow, args.isWindowsPlatform);
} else { } else {
@@ -2,7 +2,6 @@ import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
applyRuntimeOptionResultRuntime, applyRuntimeOptionResultRuntime,
cycleRuntimeOptionFromIpcRuntime,
setRuntimeOptionFromIpcRuntime, setRuntimeOptionFromIpcRuntime,
} from './runtime-options-ipc'; } from './runtime-options-ipc';
@@ -25,21 +24,3 @@ test('setRuntimeOptionFromIpcRuntime returns unavailable when manager missing',
assert.equal(result.error, 'Runtime options manager unavailable'); assert.equal(result.error, 'Runtime options manager unavailable');
assert.deepEqual(osd, []); assert.deepEqual(osd, []);
}); });
test('cycleRuntimeOptionFromIpcRuntime reports errors once', () => {
const osd: string[] = [];
const result = cycleRuntimeOptionFromIpcRuntime(
{
setOptionValue: () => ({ ok: true }),
cycleOption: () => ({ ok: false, error: 'bad option' }),
},
'anki.kikuFieldGrouping',
1,
(text) => {
osd.push(text);
},
);
assert.equal(result.ok, false);
assert.equal(result.error, 'bad option');
assert.deepEqual(osd, ['bad option']);
});
-16
View File
@@ -34,19 +34,3 @@ export function setRuntimeOptionFromIpcRuntime(
} }
return result; return result;
} }
export function cycleRuntimeOptionFromIpcRuntime(
manager: RuntimeOptionsManagerLike | null,
id: RuntimeOptionId,
direction: 1 | -1,
showMpvOsd: (text: string) => void,
): RuntimeOptionApplyResult {
if (!manager) {
return { ok: false, error: 'Runtime options manager unavailable' };
}
const result = applyRuntimeOptionResultRuntime(manager.cycleOption(id, direction), showMpvOsd);
if (!result.ok && result.error) {
showMpvOsd(result.error);
}
return result;
}
-1
View File
@@ -1,5 +1,4 @@
import type { RuntimeOptionApplyResult, RuntimeOptionId } from '../../types'; import type { RuntimeOptionApplyResult, RuntimeOptionId } from '../../types';
import type { SessionActionId } from '../../types/session-bindings';
import type { SessionActionDispatchRequest } from '../../types/runtime'; import type { SessionActionDispatchRequest } from '../../types/runtime';
export interface SessionActionExecutorDeps { 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'; 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)`; 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 assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { parseSrtCues, parseAssCues, parseSubtitleCues } from './subtitle-cue-parser'; import { parseSrtCues, parseAssCues, parseSubtitleCues } from './subtitle-cue-parser';
import type { SubtitleCue } from '../../types';
test('parseSrtCues parses basic SRT content', () => { test('parseSrtCues parses basic SRT content', () => {
const content = [ const content = [
-8
View File
@@ -609,14 +609,6 @@ function getYomitanFrequencyRank(
return null; 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); return selectBestYomitanFrequencyRank(candidateEntries);
} }
@@ -390,23 +390,6 @@ function isReduplicatedKanaSfx(text: string): boolean {
return chars.slice(0, half).join('') === chars.slice(half).join(''); 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 { function hasAdjacentKanaRepeat(text: string): boolean {
const normalized = normalizeKana(text); const normalized = normalizeKana(text);
if (!normalized) { if (!normalized) {
-65
View File
@@ -202,14 +202,9 @@ import {
createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler, createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler,
createTrayRuntimeHandlers, createTrayRuntimeHandlers,
createOverlayVisibilityRuntime, createOverlayVisibilityRuntime,
createBroadcastRuntimeOptionsChangedHandler,
createGetRuntimeOptionsStateHandler, createGetRuntimeOptionsStateHandler,
createGetFieldGroupingResolverHandler, createGetFieldGroupingResolverHandler,
createSetFieldGroupingResolverHandler, createSetFieldGroupingResolverHandler,
createOpenRuntimeOptionsPaletteHandler,
createRestorePreviousSecondarySubVisibilityHandler,
createSendToActiveOverlayWindowHandler,
createSetOverlayDebugVisualizationEnabledHandler,
createLoadSubtitlePositionHandler, createLoadSubtitlePositionHandler,
createSaveSubtitlePositionHandler, createSaveSubtitlePositionHandler,
createAppendClipboardVideoToQueueHandler, createAppendClipboardVideoToQueueHandler,
@@ -306,7 +301,6 @@ import {
handleMultiCopyDigit as handleMultiCopyDigitCore, handleMultiCopyDigit as handleMultiCopyDigitCore,
hasMpvWebsocketPlugin, hasMpvWebsocketPlugin,
importYomitanDictionaryFromZip, importYomitanDictionaryFromZip,
initializeOverlayAnkiIntegration as initializeOverlayAnkiIntegrationCore,
initializeOverlayRuntime as initializeOverlayRuntimeCore, initializeOverlayRuntime as initializeOverlayRuntimeCore,
isSubtitleAnnotationUpgrade, isSubtitleAnnotationUpgrade,
isOverlayWindowContentReady, isOverlayWindowContentReady,
@@ -613,7 +607,6 @@ import {
} from './main/anilist-url-guard'; } from './main/anilist-url-guard';
import { import {
ConfigService, ConfigService,
ConfigStartupParseError,
DEFAULT_CONFIG, DEFAULT_CONFIG,
DEFAULT_KEYBINDINGS, DEFAULT_KEYBINDINGS,
generateConfigTemplate, generateConfigTemplate,
@@ -625,7 +618,6 @@ import {
createSubtitlePrefetchService, createSubtitlePrefetchService,
type SubtitlePrefetchService, type SubtitlePrefetchService,
} from './core/services/subtitle-prefetch'; } from './core/services/subtitle-prefetch';
import { buildSubtitleSidebarSourceKey } from './main/runtime/subtitle-prefetch-source';
import { createSubtitlePrefetchInitController } from './main/runtime/subtitle-prefetch-init'; import { createSubtitlePrefetchInitController } from './main/runtime/subtitle-prefetch-init';
import { import {
createCachedInternalSubtitleTrackExtractor, createCachedInternalSubtitleTrackExtractor,
@@ -633,7 +625,6 @@ import {
} from './main/runtime/internal-subtitle-extraction'; } from './main/runtime/internal-subtitle-extraction';
import { createRemoteMediaPathDetector } from './main/runtime/network-media-path'; import { createRemoteMediaPathDetector } from './main/runtime/network-media-path';
import { applyCharacterDictionarySelection } from './main/character-dictionary-selection'; import { applyCharacterDictionarySelection } from './main/character-dictionary-selection';
import { getSubsyncConfig } from './subsync/utils';
if (process.platform === 'linux') { if (process.platform === 'linux') {
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal'); app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal');
@@ -2882,11 +2873,6 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
getOverlayInteractionActive: () => getOverlayInteractionActive: () =>
visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive(), visibleOverlayInteractionRuntime.getVisibleOverlayInteractionActive(),
getWindowTracker: () => appState.windowTracker, getWindowTracker: () => appState.windowTracker,
getLastKnownWindowsForegroundProcessName: () =>
visibleOverlayInteractionRuntime.getLastWindowsVisibleOverlayForegroundProcessName(),
getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
getWindowsFocusHandoffGraceActive: () =>
visibleOverlayInteractionRuntime.hasWindowsVisibleOverlayFocusHandoffGrace(),
getMacOSForegroundProbeActive: () => getMacOSForegroundProbeActive: () =>
visibleOverlayInteractionRuntime.getMacOSVisibleOverlayForegroundProbeActive(), visibleOverlayInteractionRuntime.getMacOSVisibleOverlayForegroundProbeActive(),
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown, getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
@@ -3317,7 +3303,6 @@ const {
cleanupJellyfinSubtitleCache, cleanupJellyfinSubtitleCache,
runJellyfinCommand, runJellyfinCommand,
openJellyfinSetupWindow, openJellyfinSetupWindow,
getJellyfinClientInfo,
ensureMpvConnectedForPlayback, ensureMpvConnectedForPlayback,
} = composeJellyfinRuntimeHandlers({ } = composeJellyfinRuntimeHandlers({
getResolvedJellyfinConfigMainDeps: { getResolvedJellyfinConfigMainDeps: {
@@ -4895,7 +4880,6 @@ const {
autoplaySubtitlePrimingRuntime.resetAutoplaySubtitlePrime(); autoplaySubtitlePrimingRuntime.resetAutoplaySubtitlePrime();
lastObservedTimePos = 0; lastObservedTimePos = 0;
appState.currentSubText = ''; appState.currentSubText = '';
appState.currentSubAssText = '';
appState.currentSubtitleData = null; appState.currentSubtitleData = null;
if (!preserveParsedSubtitleCues) { if (!preserveParsedSubtitleCues) {
appState.activeParsedSubtitleCues = []; appState.activeParsedSubtitleCues = [];
@@ -5919,7 +5903,6 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
stopMediaTimingReviewPreview: (reviewId) => mediaTimingReviewRuntime.stopPreview(reviewId), stopMediaTimingReviewPreview: (reviewId) => mediaTimingReviewRuntime.stopPreview(reviewId),
resolveMediaTimingReview: (request) => mediaTimingReviewRuntime.resolveReview(request), resolveMediaTimingReview: (request) => mediaTimingReviewRuntime.resolveReview(request),
getMainWindow: () => overlayManager.getMainWindow(), getMainWindow: () => overlayManager.getMainWindow(),
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
focusMainWindow: () => { focusMainWindow: () => {
const mainWindow = overlayManager.getMainWindow(); const mainWindow = overlayManager.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) return; if (!mainWindow || mainWindow.isDestroyed()) return;
@@ -6033,14 +6016,11 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
} }
}, },
onYoutubePickerResolve: (request) => youtubeFlowRuntime.resolveActivePicker(request), onYoutubePickerResolve: (request) => youtubeFlowRuntime.resolveActivePicker(request),
openYomitanSettings: () => openDictionarySettings(),
// Overlay lookups carry no cue context of their own; fall back to snapshotting the // Overlay lookups carry no cue context of their own; fall back to snapshotting the
// live mpv sub timings at lookup time so media generation clips the mined line even // live mpv sub timings at lookup time so media generation clips the mined line even
// when extraction finishes long after playback has moved on. // when extraction finishes long after playback has moved on.
recordSubtitleMiningContext: (context) => recordSubtitleMiningContext: (context) =>
recordSubtitleMiningContext(context ?? captureCurrentPrimarySubtitleMiningContext()), recordSubtitleMiningContext(context ?? captureCurrentPrimarySubtitleMiningContext()),
quitApp: () => requestAppQuit(),
toggleVisibleOverlay: () => toggleVisibleOverlay(),
tokenizeCurrentSubtitle: async () => { tokenizeCurrentSubtitle: async () => {
const tokenizeSubtitleForCurrent = tokenizeSubtitleDeferred; const tokenizeSubtitleForCurrent = tokenizeSubtitleDeferred;
return resolveCurrentSubtitleForRenderer({ return resolveCurrentSubtitleForRenderer({
@@ -6058,7 +6038,6 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
}); });
}, },
getCurrentSubtitleRaw: () => appState.currentSubText, getCurrentSubtitleRaw: () => appState.currentSubText,
getCurrentSubtitleAss: () => appState.currentSubAssText,
getSubtitleSidebarOpen: () => subtitleSidebarRequestedOpen, getSubtitleSidebarOpen: () => subtitleSidebarRequestedOpen,
getSubtitleSidebarPlaybackState: () => getSubtitleSidebarPlaybackState(), getSubtitleSidebarPlaybackState: () => getSubtitleSidebarPlaybackState(),
// Full snapshot: resolves the active source over mpv IPC and re-parses it when it changed. // Full snapshot: resolves the active source over mpv IPC and re-parses it when it changed.
@@ -6138,8 +6117,6 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
return resolveSubtitleStyleForRenderer(resolvedConfig); return resolveSubtitleStyleForRenderer(resolvedConfig);
}, },
saveSubtitlePosition: (position) => saveSubtitlePosition(position), saveSubtitlePosition: (position) => saveSubtitlePosition(position),
getMecabTokenizer: () => appState.mecabTokenizer,
getKeybindings: () => appState.keybindings,
getMpvInputBindings: async () => { getMpvInputBindings: async () => {
await refreshMpvSessionBindings(); await refreshMpvSessionBindings();
return readMpvInputBindings({ return readMpvInputBindings({
@@ -6169,17 +6146,8 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
controller: applyControllerConfigUpdate(currentRawConfig.controller, update), controller: applyControllerConfigUpdate(currentRawConfig.controller, update),
}); });
}, },
saveControllerPreference: ({ preferredGamepadId, preferredGamepadLabel }) => {
configService.patchRawConfig({
controller: {
preferredGamepadId,
preferredGamepadLabel,
},
});
},
getSecondarySubMode: () => appState.secondarySubMode, getSecondarySubMode: () => appState.secondarySubMode,
getMpvClient: () => appState.mpvClient, getMpvClient: () => appState.mpvClient,
getAnkiConnectStatus: () => appState.ankiIntegration !== null,
getRuntimeOptions: () => getRuntimeOptionsStateHandler(), getRuntimeOptions: () => getRuntimeOptionsStateHandler(),
reportOverlayContentBounds: (payload: unknown) => { reportOverlayContentBounds: (payload: unknown) => {
if (overlayContentMeasurementStore.report(payload)) { if (overlayContentMeasurementStore.report(payload)) {
@@ -6267,47 +6235,14 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
} }
return result; return result;
}, },
appendClipboardVideoToQueue: () => appendClipboardVideoToQueueHandler(),
getChangelogSnapshot: (options) => getChangelogSnapshot(options), getChangelogSnapshot: (options) => getChangelogSnapshot(options),
...playlistBrowserMainDeps, ...playlistBrowserMainDeps,
getImmersionTracker: () => appState.immersionTracker, getImmersionTracker: () => appState.immersionTracker,
}, },
ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({ ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({
patchAnkiConnectEnabled: (enabled: boolean) => {
configService.patchRawConfig({ ankiConnect: { enabled } });
},
getResolvedConfig: () => configService.getConfig(), getResolvedConfig: () => configService.getConfig(),
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
getMpvClient: () => appState.mpvClient, getMpvClient: () => appState.mpvClient,
getAnkiIntegration: () => appState.ankiIntegration, getAnkiIntegration: () => appState.ankiIntegration,
setAnkiIntegration: (integration: AnkiIntegration | null) => {
appState.ankiIntegration = integration;
appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(
refreshCurrentSubtitleAnnotations,
);
appState.ankiIntegration?.setSubtitleMiningContextConsumer(
consumePendingSubtitleMiningContext,
);
appState.ankiIntegration?.setMediaTimingReviewCallback(
mediaTimingReviewRuntime.requestReview,
);
appState.ankiIntegration?.setSentenceFuriganaGenerator(generateMiningSentenceFurigana);
},
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
getCachedMediaPath: (currentVideoPath, kind) =>
getCachedYoutubeMediaPathForCurrentPlayback(currentVideoPath, kind),
shouldRequireRemoteMediaCache: () => shouldRequireYoutubeMediaCacheForCurrentPlayback(),
getYoutubeMediaSourceUrl: () => getCurrentYoutubeMediaCacheSourceUrl(),
showDesktopNotification,
showOverlayNotification: (payload) =>
overlayNotificationsRuntime.showOverlayNotification(payload),
dismissOverlayNotification: (id) =>
overlayNotificationsRuntime.dismissOverlayNotification(id),
createFieldGroupingCallback: () => createFieldGroupingCallback(),
broadcastRuntimeOptionsChanged: () =>
overlayVisibilityComposer.broadcastRuntimeOptionsChanged(),
getFieldGroupingResolver: () => getFieldGroupingResolverHandler(), getFieldGroupingResolver: () => getFieldGroupingResolverHandler(),
setFieldGroupingResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => setFieldGroupingResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) =>
setFieldGroupingResolverHandler(resolver), setFieldGroupingResolverHandler(resolver),
-1
View File
@@ -49,7 +49,6 @@ import type {
CharacterDictionaryRuntimeDeps, CharacterDictionaryRuntimeDeps,
CharacterDictionarySnapshot, CharacterDictionarySnapshot,
CharacterDictionarySnapshotImage, CharacterDictionarySnapshotImage,
CharacterDictionarySnapshotProgress,
CharacterDictionarySnapshotProgressCallbacks, CharacterDictionarySnapshotProgressCallbacks,
CharacterDictionarySnapshotResult, CharacterDictionarySnapshotResult,
MergedCharacterDictionaryBuildResult, MergedCharacterDictionaryBuildResult,
+1 -62
View File
@@ -1,15 +1,11 @@
import { RuntimeOptionId, RuntimeOptionValue, SubsyncManualPayload } from '../types'; import { RuntimeOptionId, RuntimeOptionValue, SubsyncManualPayload } from '../types';
import type { OverlayNotificationPayload } from '../types/notification';
import { SubsyncResolvedConfig } from '../subsync/utils'; import { SubsyncResolvedConfig } from '../subsync/utils';
import type { SubsyncRuntimeDeps } from '../core/services/subsync-runner'; import type { SubsyncRuntimeDeps } from '../core/services/subsync-runner';
import type { IpcDepsRuntimeOptions } from '../core/services/ipc'; import type { IpcDepsRuntimeOptions } from '../core/services/ipc';
import type { AnkiJimakuIpcRuntimeOptions } from '../core/services/anki-jimaku'; import type { AnkiJimakuIpcRuntimeOptions } from '../core/services/anki-jimaku';
import type { CliCommandDepsRuntimeOptions } from '../core/services/cli-command'; import type { CliCommandDepsRuntimeOptions } from '../core/services/cli-command';
import type { HandleMpvCommandFromIpcOptions } from '../core/services/ipc-command'; import type { HandleMpvCommandFromIpcOptions } from '../core/services/ipc-command';
import { import { setRuntimeOptionFromIpcRuntime } from '../core/services/runtime-options-ipc';
cycleRuntimeOptionFromIpcRuntime,
setRuntimeOptionFromIpcRuntime,
} from '../core/services/runtime-options-ipc';
import { RuntimeOptionsManager } from '../runtime-options'; import { RuntimeOptionsManager } from '../runtime-options';
export interface RuntimeOptionsIpcDepsParams { export interface RuntimeOptionsIpcDepsParams {
@@ -28,17 +24,12 @@ export interface SubsyncRuntimeDepsParams {
export function createRuntimeOptionsIpcDeps(params: RuntimeOptionsIpcDepsParams): { export function createRuntimeOptionsIpcDeps(params: RuntimeOptionsIpcDepsParams): {
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown;
} { } {
return { return {
setRuntimeOption: (id, value) => setRuntimeOption: (id, value) =>
setRuntimeOptionFromIpcRuntime(params.getRuntimeOptionsManager(), id, value, (text) => setRuntimeOptionFromIpcRuntime(params.getRuntimeOptionsManager(), id, value, (text) =>
params.showMpvOsd(text), params.showMpvOsd(text),
), ),
cycleRuntimeOption: (id, direction) =>
cycleRuntimeOptionFromIpcRuntime(params.getRuntimeOptionsManager(), id, direction, (text) =>
params.showMpvOsd(text),
),
}; };
} }
@@ -55,7 +46,6 @@ export function createSubsyncRuntimeDeps(params: SubsyncRuntimeDepsParams): Subs
export interface MainIpcRuntimeServiceDepsParams { export interface MainIpcRuntimeServiceDepsParams {
getMainWindow: IpcDepsRuntimeOptions['getMainWindow']; getMainWindow: IpcDepsRuntimeOptions['getMainWindow'];
getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility'];
onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed']; onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed'];
onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened']; onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened'];
onOverlayMouseInteractionChanged?: IpcDepsRuntimeOptions['onOverlayMouseInteractionChanged']; onOverlayMouseInteractionChanged?: IpcDepsRuntimeOptions['onOverlayMouseInteractionChanged'];
@@ -67,12 +57,8 @@ export interface MainIpcRuntimeServiceDepsParams {
getMediaTimingReviewWaveform?: IpcDepsRuntimeOptions['getMediaTimingReviewWaveform']; getMediaTimingReviewWaveform?: IpcDepsRuntimeOptions['getMediaTimingReviewWaveform'];
stopMediaTimingReviewPreview?: IpcDepsRuntimeOptions['stopMediaTimingReviewPreview']; stopMediaTimingReviewPreview?: IpcDepsRuntimeOptions['stopMediaTimingReviewPreview'];
resolveMediaTimingReview?: IpcDepsRuntimeOptions['resolveMediaTimingReview']; resolveMediaTimingReview?: IpcDepsRuntimeOptions['resolveMediaTimingReview'];
openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings'];
quitApp: IpcDepsRuntimeOptions['quitApp'];
toggleVisibleOverlay: IpcDepsRuntimeOptions['toggleVisibleOverlay'];
tokenizeCurrentSubtitle: IpcDepsRuntimeOptions['tokenizeCurrentSubtitle']; tokenizeCurrentSubtitle: IpcDepsRuntimeOptions['tokenizeCurrentSubtitle'];
getCurrentSubtitleRaw: IpcDepsRuntimeOptions['getCurrentSubtitleRaw']; getCurrentSubtitleRaw: IpcDepsRuntimeOptions['getCurrentSubtitleRaw'];
getCurrentSubtitleAss: IpcDepsRuntimeOptions['getCurrentSubtitleAss'];
getSubtitleSidebarSnapshot?: IpcDepsRuntimeOptions['getSubtitleSidebarSnapshot']; getSubtitleSidebarSnapshot?: IpcDepsRuntimeOptions['getSubtitleSidebarSnapshot'];
getSubtitleSidebarPlaybackState?: IpcDepsRuntimeOptions['getSubtitleSidebarPlaybackState']; getSubtitleSidebarPlaybackState?: IpcDepsRuntimeOptions['getSubtitleSidebarPlaybackState'];
getSubtitleSidebarOpen?: IpcDepsRuntimeOptions['getSubtitleSidebarOpen']; getSubtitleSidebarOpen?: IpcDepsRuntimeOptions['getSubtitleSidebarOpen'];
@@ -82,9 +68,7 @@ export interface MainIpcRuntimeServiceDepsParams {
getSubtitlePosition: IpcDepsRuntimeOptions['getSubtitlePosition']; getSubtitlePosition: IpcDepsRuntimeOptions['getSubtitlePosition'];
getSubtitleStyle: IpcDepsRuntimeOptions['getSubtitleStyle']; getSubtitleStyle: IpcDepsRuntimeOptions['getSubtitleStyle'];
saveSubtitlePosition: IpcDepsRuntimeOptions['saveSubtitlePosition']; saveSubtitlePosition: IpcDepsRuntimeOptions['saveSubtitlePosition'];
getMecabTokenizer: IpcDepsRuntimeOptions['getMecabTokenizer'];
handleMpvCommand: IpcDepsRuntimeOptions['handleMpvCommand']; handleMpvCommand: IpcDepsRuntimeOptions['handleMpvCommand'];
getKeybindings: IpcDepsRuntimeOptions['getKeybindings'];
getMpvInputBindings?: IpcDepsRuntimeOptions['getMpvInputBindings']; getMpvInputBindings?: IpcDepsRuntimeOptions['getMpvInputBindings'];
getSessionBindings: IpcDepsRuntimeOptions['getSessionBindings']; getSessionBindings: IpcDepsRuntimeOptions['getSessionBindings'];
getConfiguredShortcuts: IpcDepsRuntimeOptions['getConfiguredShortcuts']; getConfiguredShortcuts: IpcDepsRuntimeOptions['getConfiguredShortcuts'];
@@ -94,14 +78,11 @@ export interface MainIpcRuntimeServiceDepsParams {
getOverlayNotificationPosition: IpcDepsRuntimeOptions['getOverlayNotificationPosition']; getOverlayNotificationPosition: IpcDepsRuntimeOptions['getOverlayNotificationPosition'];
getControllerConfig: IpcDepsRuntimeOptions['getControllerConfig']; getControllerConfig: IpcDepsRuntimeOptions['getControllerConfig'];
saveControllerConfig: IpcDepsRuntimeOptions['saveControllerConfig']; saveControllerConfig: IpcDepsRuntimeOptions['saveControllerConfig'];
saveControllerPreference: IpcDepsRuntimeOptions['saveControllerPreference'];
getSecondarySubMode: IpcDepsRuntimeOptions['getSecondarySubMode']; getSecondarySubMode: IpcDepsRuntimeOptions['getSecondarySubMode'];
getMpvClient: IpcDepsRuntimeOptions['getMpvClient']; getMpvClient: IpcDepsRuntimeOptions['getMpvClient'];
runSubsyncManual: IpcDepsRuntimeOptions['runSubsyncManual']; runSubsyncManual: IpcDepsRuntimeOptions['runSubsyncManual'];
getAnkiConnectStatus: IpcDepsRuntimeOptions['getAnkiConnectStatus'];
getRuntimeOptions: IpcDepsRuntimeOptions['getRuntimeOptions']; getRuntimeOptions: IpcDepsRuntimeOptions['getRuntimeOptions'];
setRuntimeOption: IpcDepsRuntimeOptions['setRuntimeOption']; setRuntimeOption: IpcDepsRuntimeOptions['setRuntimeOption'];
cycleRuntimeOption: IpcDepsRuntimeOptions['cycleRuntimeOption'];
reportOverlayContentBounds: IpcDepsRuntimeOptions['reportOverlayContentBounds']; reportOverlayContentBounds: IpcDepsRuntimeOptions['reportOverlayContentBounds'];
getAnilistStatus: IpcDepsRuntimeOptions['getAnilistStatus']; getAnilistStatus: IpcDepsRuntimeOptions['getAnilistStatus'];
clearAnilistToken: IpcDepsRuntimeOptions['clearAnilistToken']; clearAnilistToken: IpcDepsRuntimeOptions['clearAnilistToken'];
@@ -115,7 +96,6 @@ export interface MainIpcRuntimeServiceDepsParams {
getCharacterDictionaryManagerSnapshot?: IpcDepsRuntimeOptions['getCharacterDictionaryManagerSnapshot']; getCharacterDictionaryManagerSnapshot?: IpcDepsRuntimeOptions['getCharacterDictionaryManagerSnapshot'];
removeCharacterDictionaryManagedEntry?: IpcDepsRuntimeOptions['removeCharacterDictionaryManagedEntry']; removeCharacterDictionaryManagedEntry?: IpcDepsRuntimeOptions['removeCharacterDictionaryManagedEntry'];
moveCharacterDictionaryManagedEntry?: IpcDepsRuntimeOptions['moveCharacterDictionaryManagedEntry']; moveCharacterDictionaryManagedEntry?: IpcDepsRuntimeOptions['moveCharacterDictionaryManagedEntry'];
appendClipboardVideoToQueue: IpcDepsRuntimeOptions['appendClipboardVideoToQueue'];
getChangelogSnapshot?: IpcDepsRuntimeOptions['getChangelogSnapshot']; getChangelogSnapshot?: IpcDepsRuntimeOptions['getChangelogSnapshot'];
getPlaylistBrowserSnapshot: IpcDepsRuntimeOptions['getPlaylistBrowserSnapshot']; getPlaylistBrowserSnapshot: IpcDepsRuntimeOptions['getPlaylistBrowserSnapshot'];
appendPlaylistBrowserFile: IpcDepsRuntimeOptions['appendPlaylistBrowserFile']; appendPlaylistBrowserFile: IpcDepsRuntimeOptions['appendPlaylistBrowserFile'];
@@ -126,22 +106,9 @@ export interface MainIpcRuntimeServiceDepsParams {
} }
export interface AnkiJimakuIpcRuntimeServiceDepsParams { export interface AnkiJimakuIpcRuntimeServiceDepsParams {
patchAnkiConnectEnabled: AnkiJimakuIpcRuntimeOptions['patchAnkiConnectEnabled'];
getResolvedConfig: AnkiJimakuIpcRuntimeOptions['getResolvedConfig']; getResolvedConfig: AnkiJimakuIpcRuntimeOptions['getResolvedConfig'];
getRuntimeOptionsManager: AnkiJimakuIpcRuntimeOptions['getRuntimeOptionsManager'];
getSubtitleTimingTracker: AnkiJimakuIpcRuntimeOptions['getSubtitleTimingTracker'];
getMpvClient: AnkiJimakuIpcRuntimeOptions['getMpvClient']; getMpvClient: AnkiJimakuIpcRuntimeOptions['getMpvClient'];
getAnkiIntegration: AnkiJimakuIpcRuntimeOptions['getAnkiIntegration']; getAnkiIntegration: AnkiJimakuIpcRuntimeOptions['getAnkiIntegration'];
setAnkiIntegration: AnkiJimakuIpcRuntimeOptions['setAnkiIntegration'];
getKnownWordCacheStatePath: AnkiJimakuIpcRuntimeOptions['getKnownWordCacheStatePath'];
getCachedMediaPath?: AnkiJimakuIpcRuntimeOptions['getCachedMediaPath'];
shouldRequireRemoteMediaCache?: AnkiJimakuIpcRuntimeOptions['shouldRequireRemoteMediaCache'];
getYoutubeMediaSourceUrl?: AnkiJimakuIpcRuntimeOptions['getYoutubeMediaSourceUrl'];
showDesktopNotification: AnkiJimakuIpcRuntimeOptions['showDesktopNotification'];
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
dismissOverlayNotification?: (id: string) => void;
createFieldGroupingCallback: AnkiJimakuIpcRuntimeOptions['createFieldGroupingCallback'];
broadcastRuntimeOptionsChanged: AnkiJimakuIpcRuntimeOptions['broadcastRuntimeOptionsChanged'];
getFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions['getFieldGroupingResolver']; getFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions['getFieldGroupingResolver'];
setFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions['setFieldGroupingResolver']; setFieldGroupingResolver: AnkiJimakuIpcRuntimeOptions['setFieldGroupingResolver'];
parseMediaInfo: AnkiJimakuIpcRuntimeOptions['parseMediaInfo']; parseMediaInfo: AnkiJimakuIpcRuntimeOptions['parseMediaInfo'];
@@ -259,7 +226,6 @@ export function createMainIpcRuntimeServiceDeps(
): IpcDepsRuntimeOptions { ): IpcDepsRuntimeOptions {
return { return {
getMainWindow: params.getMainWindow, getMainWindow: params.getMainWindow,
getVisibleOverlayVisibility: params.getVisibleOverlayVisibility,
onOverlayModalClosed: params.onOverlayModalClosed, onOverlayModalClosed: params.onOverlayModalClosed,
onOverlayModalOpened: params.onOverlayModalOpened, onOverlayModalOpened: params.onOverlayModalOpened,
onOverlayMouseInteractionChanged: params.onOverlayMouseInteractionChanged, onOverlayMouseInteractionChanged: params.onOverlayMouseInteractionChanged,
@@ -271,12 +237,8 @@ export function createMainIpcRuntimeServiceDeps(
getMediaTimingReviewWaveform: params.getMediaTimingReviewWaveform, getMediaTimingReviewWaveform: params.getMediaTimingReviewWaveform,
stopMediaTimingReviewPreview: params.stopMediaTimingReviewPreview, stopMediaTimingReviewPreview: params.stopMediaTimingReviewPreview,
resolveMediaTimingReview: params.resolveMediaTimingReview, resolveMediaTimingReview: params.resolveMediaTimingReview,
openYomitanSettings: params.openYomitanSettings,
quitApp: params.quitApp,
toggleVisibleOverlay: params.toggleVisibleOverlay,
tokenizeCurrentSubtitle: params.tokenizeCurrentSubtitle, tokenizeCurrentSubtitle: params.tokenizeCurrentSubtitle,
getCurrentSubtitleRaw: params.getCurrentSubtitleRaw, getCurrentSubtitleRaw: params.getCurrentSubtitleRaw,
getCurrentSubtitleAss: params.getCurrentSubtitleAss,
getSubtitleSidebarSnapshot: params.getSubtitleSidebarSnapshot, getSubtitleSidebarSnapshot: params.getSubtitleSidebarSnapshot,
getSubtitleSidebarPlaybackState: params.getSubtitleSidebarPlaybackState, getSubtitleSidebarPlaybackState: params.getSubtitleSidebarPlaybackState,
getSubtitleSidebarOpen: params.getSubtitleSidebarOpen, getSubtitleSidebarOpen: params.getSubtitleSidebarOpen,
@@ -284,9 +246,7 @@ export function createMainIpcRuntimeServiceDeps(
getSubtitlePosition: params.getSubtitlePosition, getSubtitlePosition: params.getSubtitlePosition,
getSubtitleStyle: params.getSubtitleStyle, getSubtitleStyle: params.getSubtitleStyle,
saveSubtitlePosition: params.saveSubtitlePosition, saveSubtitlePosition: params.saveSubtitlePosition,
getMecabTokenizer: params.getMecabTokenizer,
handleMpvCommand: params.handleMpvCommand, handleMpvCommand: params.handleMpvCommand,
getKeybindings: params.getKeybindings,
getMpvInputBindings: params.getMpvInputBindings, getMpvInputBindings: params.getMpvInputBindings,
getSessionBindings: params.getSessionBindings, getSessionBindings: params.getSessionBindings,
getConfiguredShortcuts: params.getConfiguredShortcuts, getConfiguredShortcuts: params.getConfiguredShortcuts,
@@ -296,16 +256,13 @@ export function createMainIpcRuntimeServiceDeps(
getOverlayNotificationPosition: params.getOverlayNotificationPosition, getOverlayNotificationPosition: params.getOverlayNotificationPosition,
getControllerConfig: params.getControllerConfig, getControllerConfig: params.getControllerConfig,
saveControllerConfig: params.saveControllerConfig, saveControllerConfig: params.saveControllerConfig,
saveControllerPreference: params.saveControllerPreference,
focusMainWindow: params.focusMainWindow ?? (() => {}), focusMainWindow: params.focusMainWindow ?? (() => {}),
activatePlaybackWindowForOverlayInteraction: params.activatePlaybackWindowForOverlayInteraction, activatePlaybackWindowForOverlayInteraction: params.activatePlaybackWindowForOverlayInteraction,
getSecondarySubMode: params.getSecondarySubMode, getSecondarySubMode: params.getSecondarySubMode,
getMpvClient: params.getMpvClient, getMpvClient: params.getMpvClient,
runSubsyncManual: params.runSubsyncManual, runSubsyncManual: params.runSubsyncManual,
getAnkiConnectStatus: params.getAnkiConnectStatus,
getRuntimeOptions: params.getRuntimeOptions, getRuntimeOptions: params.getRuntimeOptions,
setRuntimeOption: params.setRuntimeOption, setRuntimeOption: params.setRuntimeOption,
cycleRuntimeOption: params.cycleRuntimeOption,
reportOverlayContentBounds: params.reportOverlayContentBounds, reportOverlayContentBounds: params.reportOverlayContentBounds,
getAnilistStatus: params.getAnilistStatus, getAnilistStatus: params.getAnilistStatus,
clearAnilistToken: params.clearAnilistToken, clearAnilistToken: params.clearAnilistToken,
@@ -319,7 +276,6 @@ export function createMainIpcRuntimeServiceDeps(
getCharacterDictionaryManagerSnapshot: params.getCharacterDictionaryManagerSnapshot, getCharacterDictionaryManagerSnapshot: params.getCharacterDictionaryManagerSnapshot,
removeCharacterDictionaryManagedEntry: params.removeCharacterDictionaryManagedEntry, removeCharacterDictionaryManagedEntry: params.removeCharacterDictionaryManagedEntry,
moveCharacterDictionaryManagedEntry: params.moveCharacterDictionaryManagedEntry, moveCharacterDictionaryManagedEntry: params.moveCharacterDictionaryManagedEntry,
appendClipboardVideoToQueue: params.appendClipboardVideoToQueue,
getChangelogSnapshot: params.getChangelogSnapshot, getChangelogSnapshot: params.getChangelogSnapshot,
getPlaylistBrowserSnapshot: params.getPlaylistBrowserSnapshot, getPlaylistBrowserSnapshot: params.getPlaylistBrowserSnapshot,
appendPlaylistBrowserFile: params.appendPlaylistBrowserFile, appendPlaylistBrowserFile: params.appendPlaylistBrowserFile,
@@ -334,26 +290,9 @@ export function createAnkiJimakuIpcRuntimeServiceDeps(
params: AnkiJimakuIpcRuntimeServiceDepsParams, params: AnkiJimakuIpcRuntimeServiceDepsParams,
): AnkiJimakuIpcRuntimeOptions { ): AnkiJimakuIpcRuntimeOptions {
return { return {
patchAnkiConnectEnabled: params.patchAnkiConnectEnabled,
getResolvedConfig: params.getResolvedConfig, getResolvedConfig: params.getResolvedConfig,
getRuntimeOptionsManager: params.getRuntimeOptionsManager,
getSubtitleTimingTracker: params.getSubtitleTimingTracker,
getMpvClient: params.getMpvClient, getMpvClient: params.getMpvClient,
getAnkiIntegration: params.getAnkiIntegration, getAnkiIntegration: params.getAnkiIntegration,
setAnkiIntegration: params.setAnkiIntegration,
getKnownWordCacheStatePath: params.getKnownWordCacheStatePath,
...(params.getCachedMediaPath ? { getCachedMediaPath: params.getCachedMediaPath } : {}),
...(params.shouldRequireRemoteMediaCache
? { shouldRequireRemoteMediaCache: params.shouldRequireRemoteMediaCache }
: {}),
...(params.getYoutubeMediaSourceUrl
? { getYoutubeMediaSourceUrl: params.getYoutubeMediaSourceUrl }
: {}),
showDesktopNotification: params.showDesktopNotification,
showOverlayNotification: params.showOverlayNotification,
dismissOverlayNotification: params.dismissOverlayNotification,
createFieldGroupingCallback: params.createFieldGroupingCallback,
broadcastRuntimeOptionsChanged: params.broadcastRuntimeOptionsChanged,
getFieldGroupingResolver: params.getFieldGroupingResolver, getFieldGroupingResolver: params.getFieldGroupingResolver,
setFieldGroupingResolver: params.setFieldGroupingResolver, setFieldGroupingResolver: params.setFieldGroupingResolver,
parseMediaInfo: params.parseMediaInfo, parseMediaInfo: params.parseMediaInfo,
+1 -2
View File
@@ -15,7 +15,7 @@ import {
export interface RegisterIpcRuntimeServicesParams { export interface RegisterIpcRuntimeServicesParams {
runtimeOptions: RuntimeOptionsIpcDepsParams; runtimeOptions: RuntimeOptionsIpcDepsParams;
mainDeps: Omit<MainIpcRuntimeServiceDepsParams, 'setRuntimeOption' | 'cycleRuntimeOption'>; mainDeps: Omit<MainIpcRuntimeServiceDepsParams, 'setRuntimeOption'>;
ankiJimakuDeps: AnkiJimakuIpcRuntimeServiceDepsParams; ankiJimakuDeps: AnkiJimakuIpcRuntimeServiceDepsParams;
} }
@@ -40,7 +40,6 @@ export function registerIpcRuntimeServices(params: RegisterIpcRuntimeServicesPar
registerMainIpcRuntimeServices({ registerMainIpcRuntimeServices({
...params.mainDeps, ...params.mainDeps,
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption, setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
}); });
registerAnkiJimakuIpcRuntimeServices(params.ankiJimakuDeps); registerAnkiJimakuIpcRuntimeServices(params.ankiJimakuDeps);
} }
-1
View File
@@ -37,7 +37,6 @@ test('media path changes clear rendered subtitle state without clearing same-you
assert.ok(actionBlock); assert.ok(actionBlock);
assert.match(actionBlock, /appState\.currentSubText = '';/); assert.match(actionBlock, /appState\.currentSubText = '';/);
assert.match(actionBlock, /appState\.currentSubAssText = '';/);
assert.match(actionBlock, /appState\.currentSubtitleData = null;/); assert.match(actionBlock, /appState\.currentSubtitleData = null;/);
assert.match(actionBlock, /isSameYoutubeMediaPath\(/); assert.match(actionBlock, /isSameYoutubeMediaPath\(/);
assert.match(actionBlock, /if \(!preserveParsedSubtitleCues\)/); assert.match(actionBlock, /if \(!preserveParsedSubtitleCues\)/);
-6
View File
@@ -15,9 +15,6 @@ export interface OverlayVisibilityRuntimeDeps {
getSuspendVisibleOverlay?: () => boolean; getSuspendVisibleOverlay?: () => boolean;
getOverlayInteractionActive?: () => boolean; getOverlayInteractionActive?: () => boolean;
getWindowTracker: () => BaseWindowTracker | null; getWindowTracker: () => BaseWindowTracker | null;
getLastKnownWindowsForegroundProcessName?: () => string | null;
getWindowsOverlayProcessName?: () => string | null;
getWindowsFocusHandoffGraceActive?: () => boolean;
getMacOSForegroundProbeActive?: () => boolean; getMacOSForegroundProbeActive?: () => boolean;
getTrackerNotReadyWarningShown: () => boolean; getTrackerNotReadyWarningShown: () => boolean;
setTrackerNotReadyWarningShown: (shown: boolean) => void; setTrackerNotReadyWarningShown: (shown: boolean) => void;
@@ -61,9 +58,6 @@ export function createOverlayVisibilityRuntimeService(
overlayInteractionActive: deps.getOverlayInteractionActive?.() ?? false, overlayInteractionActive: deps.getOverlayInteractionActive?.() ?? false,
mainWindow, mainWindow,
windowTracker, windowTracker,
lastKnownWindowsForegroundProcessName: deps.getLastKnownWindowsForegroundProcessName?.(),
windowsOverlayProcessName: deps.getWindowsOverlayProcessName?.() ?? null,
windowsFocusHandoffGraceActive: deps.getWindowsFocusHandoffGraceActive?.() ?? false,
macOSForegroundProbeActive: deps.getMacOSForegroundProbeActive?.() ?? false, macOSForegroundProbeActive: deps.getMacOSForegroundProbeActive?.() ?? false,
trackerNotReadyWarningShown: deps.getTrackerNotReadyWarningShown(), trackerNotReadyWarningShown: deps.getTrackerNotReadyWarningShown(),
setTrackerNotReadyWarningShown: (shown: boolean) => { setTrackerNotReadyWarningShown: (shown: boolean) => {
@@ -45,7 +45,6 @@ function createDefaultMpvFixture() {
immersionTracker: null, immersionTracker: null,
subtitleTimingTracker: null, subtitleTimingTracker: null,
currentSubText: '', currentSubText: '',
currentSubAssText: '',
playbackPaused: null, playbackPaused: null,
previousSecondarySubVisibility: null, previousSecondarySubVisibility: null,
}, },
@@ -1,7 +1,6 @@
type MpvBindingEventName = type MpvBindingEventName =
| 'connection-change' | 'connection-change'
| 'subtitle-change' | 'subtitle-change'
| 'subtitle-ass-change'
| 'secondary-subtitle-change' | 'secondary-subtitle-change'
| 'subtitle-track-change' | 'subtitle-track-change'
| 'secondary-subtitle-track-change' | 'secondary-subtitle-track-change'
@@ -89,7 +88,6 @@ export function createHandleMpvSubtitleTimingHandler(deps: {
export function createBindMpvClientEventHandlers(deps: { export function createBindMpvClientEventHandlers(deps: {
onConnectionChange: (payload: { connected: boolean }) => void; onConnectionChange: (payload: { connected: boolean }) => void;
onSubtitleChange: (payload: { text: string }) => void; onSubtitleChange: (payload: { text: string }) => void;
onSubtitleAssChange: (payload: { text: string }) => void;
onSecondarySubtitleChange: (payload: { text: string }) => void; onSecondarySubtitleChange: (payload: { text: string }) => void;
onSubtitleTrackChange: (payload: { sid: number | null }) => void; onSubtitleTrackChange: (payload: { sid: number | null }) => void;
onSecondarySubtitleTrackChange: (payload: { sid: number | null }) => void; onSecondarySubtitleTrackChange: (payload: { sid: number | null }) => void;
@@ -108,7 +106,6 @@ export function createBindMpvClientEventHandlers(deps: {
return (mpvClient: MpvEventClient): void => { return (mpvClient: MpvEventClient): void => {
mpvClient.on('connection-change', deps.onConnectionChange); mpvClient.on('connection-change', deps.onConnectionChange);
mpvClient.on('subtitle-change', deps.onSubtitleChange); mpvClient.on('subtitle-change', deps.onSubtitleChange);
mpvClient.on('subtitle-ass-change', deps.onSubtitleAssChange);
mpvClient.on('secondary-subtitle-change', deps.onSecondarySubtitleChange); mpvClient.on('secondary-subtitle-change', deps.onSecondarySubtitleChange);
mpvClient.on('subtitle-track-change', deps.onSubtitleTrackChange); mpvClient.on('subtitle-track-change', deps.onSubtitleTrackChange);
mpvClient.on('secondary-subtitle-track-change', deps.onSecondarySubtitleTrackChange); mpvClient.on('secondary-subtitle-track-change', deps.onSecondarySubtitleTrackChange);
@@ -49,16 +49,6 @@ export function createHandleMpvSubtitleChangeHandler(deps: {
}; };
} }
export function createHandleMpvSubtitleAssChangeHandler(deps: {
setCurrentSubAssText: (text: string) => void;
broadcastSubtitleAss: (text: string) => void;
}) {
return ({ text }: { text: string }): void => {
deps.setCurrentSubAssText(text);
deps.broadcastSubtitleAss(text);
};
}
export function createHandleMpvSecondarySubtitleChangeHandler(deps: { export function createHandleMpvSecondarySubtitleChangeHandler(deps: {
broadcastSecondarySubtitle: (text: string) => void; broadcastSecondarySubtitle: (text: string) => void;
}) { }) {
@@ -29,8 +29,6 @@ function createDeps(overrides: Partial<Deps> = {}): Deps {
onSubtitleChange: () => {}, onSubtitleChange: () => {},
refreshDiscordPresence: () => {}, refreshDiscordPresence: () => {},
setCurrentSubAssText: () => {},
broadcastSubtitleAss: () => {},
broadcastSecondarySubtitle: () => {}, broadcastSecondarySubtitle: () => {},
updateCurrentMediaPath: () => {}, updateCurrentMediaPath: () => {},
@@ -10,7 +10,6 @@ import {
createHandleMpvPauseChangeHandler, createHandleMpvPauseChangeHandler,
createHandleMpvSecondarySubtitleChangeHandler, createHandleMpvSecondarySubtitleChangeHandler,
createHandleMpvSecondarySubtitleVisibilityHandler, createHandleMpvSecondarySubtitleVisibilityHandler,
createHandleMpvSubtitleAssChangeHandler,
createHandleMpvSubtitleChangeHandler, createHandleMpvSubtitleChangeHandler,
createHandleMpvSubtitleMetricsChangeHandler, createHandleMpvSubtitleMetricsChangeHandler,
createHandleMpvTimePosChangeHandler, createHandleMpvTimePosChangeHandler,
@@ -52,8 +51,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
logSubtitleProcessingDebug?: (message: string) => void; logSubtitleProcessingDebug?: (message: string) => void;
refreshDiscordPresence: () => void; refreshDiscordPresence: () => void;
setCurrentSubAssText: (text: string) => void;
broadcastSubtitleAss: (text: string) => void;
broadcastSecondarySubtitle: (text: string) => void; broadcastSecondarySubtitle: (text: string) => void;
onSubtitleTrackChange?: (sid: number | null) => void; onSubtitleTrackChange?: (sid: number | null) => void;
onSecondarySubtitleTrackChange?: (sid: number | null) => void; onSecondarySubtitleTrackChange?: (sid: number | null) => void;
@@ -135,10 +132,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
: undefined, : undefined,
refreshDiscordPresence: () => deps.refreshDiscordPresence(), refreshDiscordPresence: () => deps.refreshDiscordPresence(),
}); });
const handleMpvSubtitleAssChange = createHandleMpvSubtitleAssChangeHandler({
setCurrentSubAssText: (text) => deps.setCurrentSubAssText(text),
broadcastSubtitleAss: (text) => deps.broadcastSubtitleAss(text),
});
const handleMpvSecondarySubtitleChange = createHandleMpvSecondarySubtitleChangeHandler({ const handleMpvSecondarySubtitleChange = createHandleMpvSecondarySubtitleChangeHandler({
broadcastSecondarySubtitle: (text) => deps.broadcastSecondarySubtitle(text), broadcastSecondarySubtitle: (text) => deps.broadcastSecondarySubtitle(text),
}); });
@@ -200,7 +193,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
createBindMpvClientEventHandlers({ createBindMpvClientEventHandlers({
onConnectionChange: handleMpvConnectionChangeWithSidebarReset, onConnectionChange: handleMpvConnectionChangeWithSidebarReset,
onSubtitleChange: handleMpvSubtitleChange, onSubtitleChange: handleMpvSubtitleChange,
onSubtitleAssChange: handleMpvSubtitleAssChange,
onSecondarySubtitleChange: handleMpvSecondarySubtitleChange, onSecondarySubtitleChange: handleMpvSecondarySubtitleChange,
onSubtitleTrackChange: ({ sid }) => deps.onSubtitleTrackChange?.(sid), onSubtitleTrackChange: ({ sid }) => deps.onSubtitleTrackChange?.(sid),
onSecondarySubtitleTrackChange: ({ sid }) => deps.onSecondarySubtitleTrackChange?.(sid), onSecondarySubtitleTrackChange: ({ sid }) => deps.onSecondarySubtitleTrackChange?.(sid),
@@ -20,7 +20,6 @@ function buildMainDeps(
immersionTracker: null, immersionTracker: null,
subtitleTimingTracker: null, subtitleTimingTracker: null,
currentSubText: '', currentSubText: '',
currentSubAssText: '',
playbackPaused: null, playbackPaused: null,
previousSecondarySubVisibility: false, previousSecondarySubVisibility: false,
...appStateOverrides, ...appStateOverrides,
@@ -46,7 +46,6 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
activeParsedSubtitleSource?: string | null; activeParsedSubtitleSource?: string | null;
currentMediaPath?: string | null; currentMediaPath?: string | null;
currentSubText: string; currentSubText: string;
currentSubAssText: string;
currentSubtitleData?: SubtitleData | null; currentSubtitleData?: SubtitleData | null;
playbackPaused: boolean | null; playbackPaused: boolean | null;
previousSecondarySubVisibility: boolean | null; previousSecondarySubVisibility: boolean | null;
@@ -300,11 +299,6 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
? (trackList: unknown[] | null) => deps.onSubtitleTrackListChange!(trackList) ? (trackList: unknown[] | null) => deps.onSubtitleTrackListChange!(trackList)
: undefined, : undefined,
refreshDiscordPresence: () => deps.refreshDiscordPresence(), refreshDiscordPresence: () => deps.refreshDiscordPresence(),
setCurrentSubAssText: (text: string) => {
deps.appState.currentSubAssText = text;
},
broadcastSubtitleAss: (text: string) =>
deps.broadcastToOverlayWindows('subtitle-ass:set', text),
broadcastSecondarySubtitle: (text: string) => { broadcastSecondarySubtitle: (text: string) => {
if (deps.onSecondarySubtitleChange) { if (deps.onSecondarySubtitleChange) {
deps.onSecondarySubtitleChange(text); deps.onSecondarySubtitleChange(text);
@@ -14,10 +14,6 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
getSuspendVisibleOverlay: () => deps.getSuspendVisibleOverlay?.() ?? false, getSuspendVisibleOverlay: () => deps.getSuspendVisibleOverlay?.() ?? false,
getOverlayInteractionActive: () => deps.getOverlayInteractionActive?.() ?? false, getOverlayInteractionActive: () => deps.getOverlayInteractionActive?.() ?? false,
getWindowTracker: () => deps.getWindowTracker(), getWindowTracker: () => deps.getWindowTracker(),
getLastKnownWindowsForegroundProcessName: () =>
deps.getLastKnownWindowsForegroundProcessName?.() ?? null,
getWindowsOverlayProcessName: () => deps.getWindowsOverlayProcessName?.() ?? null,
getWindowsFocusHandoffGraceActive: () => deps.getWindowsFocusHandoffGraceActive?.() ?? false,
getMacOSForegroundProbeActive: () => deps.getMacOSForegroundProbeActive?.() ?? false, getMacOSForegroundProbeActive: () => deps.getMacOSForegroundProbeActive?.() ?? false,
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(), getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown), 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 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_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_FOREGROUND_POLL_INTERVAL_MS = 75;
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500; const LINUX_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 1_500;
// Ignore transient "neither mpv nor overlay is the active window" blips before suppressing // 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 // 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 windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
let windowsOverlayPointerInteractionActive = false; let windowsOverlayPointerInteractionActive = false;
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null; let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
let lastWindowsVisibleOverlayBlurredAtMs = 0;
let lastLinuxVisibleOverlayFollowedMpvAtMs = 0; let lastLinuxVisibleOverlayFollowedMpvAtMs = 0;
const linuxPointerForegroundSuppressionGrace: ForegroundSuppressionGraceState = { const linuxPointerForegroundSuppressionGrace: ForegroundSuppressionGraceState = {
lossSinceMs: null, 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 { function shouldPollWindowsVisibleOverlayForegroundProcess(): boolean {
if (process.platform !== 'win32' || !overlayManager.getVisibleOverlayVisible()) { if (process.platform !== 'win32' || !overlayManager.getVisibleOverlayVisible()) {
return false; return false;
@@ -562,9 +551,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
return; return;
} }
if (process.platform === 'win32') {
lastWindowsVisibleOverlayBlurredAtMs = Date.now();
}
startMacOSVisibleOverlayForegroundProbe(); startMacOSVisibleOverlayForegroundProbe();
clearVisibleOverlayBlurRefreshTimeouts(); clearVisibleOverlayBlurRefreshTimeouts();
for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) { for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) {
@@ -860,7 +846,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
retargetOverlayWindowTrackerForMpvSocket, retargetOverlayWindowTrackerForMpvSocket,
requestWindowsVisibleOverlayZOrderSync, requestWindowsVisibleOverlayZOrderSync,
scheduleWindowsVisibleOverlayZOrderSyncBurst, scheduleWindowsVisibleOverlayZOrderSyncBurst,
hasWindowsVisibleOverlayFocusHandoffGrace,
ensureWindowsVisibleOverlayForegroundPollLoop, ensureWindowsVisibleOverlayForegroundPollLoop,
clearWindowsVisibleOverlayForegroundPollLoop, clearWindowsVisibleOverlayForegroundPollLoop,
scheduleVisibleOverlayBlurRefresh, scheduleVisibleOverlayBlurRefresh,
@@ -881,8 +866,6 @@ export function createVisibleOverlayInteractionRuntime(deps: VisibleOverlayInter
windowsOverlayPointerInteractionActive = false; windowsOverlayPointerInteractionActive = false;
}, },
getLinuxOverlayInputShapeActive: () => linuxOverlayInputShapeActive, getLinuxOverlayInputShapeActive: () => linuxOverlayInputShapeActive,
getLastWindowsVisibleOverlayForegroundProcessName: () =>
lastWindowsVisibleOverlayForegroundProcessName,
getMacOSVisibleOverlayForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive, getMacOSVisibleOverlayForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive,
setLinuxOverlayInteractiveHint: (interactive: boolean) => { setLinuxOverlayInteractiveHint: (interactive: boolean) => {
linuxOverlayInteractiveHint = interactive; linuxOverlayInteractiveHint = interactive;
-2
View File
@@ -160,7 +160,6 @@ export interface AppState {
discordPresenceService: ReturnType<typeof createDiscordPresenceService> | null; discordPresenceService: ReturnType<typeof createDiscordPresenceService> | null;
reconnectTimer: ReturnType<typeof setTimeout> | null; reconnectTimer: ReturnType<typeof setTimeout> | null;
currentSubText: string; currentSubText: string;
currentSubAssText: string;
currentSubtitleData: SubtitleData | null; currentSubtitleData: SubtitleData | null;
activeParsedSubtitleCues: SubtitleCue[]; activeParsedSubtitleCues: SubtitleCue[];
activeParsedSubtitleSource: string | null; activeParsedSubtitleSource: string | null;
@@ -247,7 +246,6 @@ export function createAppState(values: AppStateInitialValues): AppState {
discordPresenceService: null, discordPresenceService: null,
reconnectTimer: null, reconnectTimer: null,
currentSubText: '', currentSubText: '',
currentSubAssText: '',
currentSubtitleData: null, currentSubtitleData: null,
activeParsedSubtitleCues: [], activeParsedSubtitleCues: [],
activeParsedSubtitleSource: null, activeParsedSubtitleSource: null,
-23
View File
@@ -399,29 +399,6 @@ export class MecabTokenizer {
path: this.mecabPath, path: this.mecabPath,
}; };
} }
setEnabled(enabled: boolean): void {
this.enabled = enabled;
if (!enabled) {
const pending: MecabQueuedRequest[] = [];
if (this.activeRequest) {
pending.push({
text: this.activeRequest.text,
retryCount: MecabTokenizer.MAX_RETRY_COUNT,
resolve: this.activeRequest.resolve,
});
}
if (this.requestQueue.length > 0) {
pending.push(...this.requestQueue);
}
this.activeRequest = null;
this.requestQueue = [];
for (const request of pending) {
request.resolve(null);
}
this.stopPersistentProcess();
}
}
} }
export { mapMecabPos1ToPartOfSpeech as mapPartOfSpeech }; export { mapMecabPos1ToPartOfSpeech as mapPartOfSpeech };
-59
View File
@@ -60,8 +60,6 @@ window.addEventListener('hachidori-open-external', (event) => {
import type { import type {
SubtitleData, SubtitleData,
SubtitlePosition, SubtitlePosition,
MecabStatus,
Keybinding,
ElectronAPI, ElectronAPI,
SecondarySubMode, SecondarySubMode,
SubtitleStyleConfig, SubtitleStyleConfig,
@@ -83,7 +81,6 @@ import type {
SubsyncManualPayload, SubsyncManualPayload,
SubsyncManualRunRequest, SubsyncManualRunRequest,
SubsyncResult, SubsyncResult,
ClipboardAppendResult,
PlaylistBrowserMutationResult, PlaylistBrowserMutationResult,
PlaylistBrowserSnapshot, PlaylistBrowserSnapshot,
KikuFieldGroupingRequestData, KikuFieldGroupingRequestData,
@@ -98,7 +95,6 @@ import type {
ShortcutsConfig, ShortcutsConfig,
ConfigHotReloadPayload, ConfigHotReloadPayload,
ControllerConfigUpdate, ControllerConfigUpdate,
ControllerPreferenceUpdate,
ResolvedControllerConfig, ResolvedControllerConfig,
SessionHelpOpenPayload, SessionHelpOpenPayload,
SessionNumericSelectionStartPayload, SessionNumericSelectionStartPayload,
@@ -214,10 +210,6 @@ const onOverlayNotificationEvent =
const onNotificationHistoryToggleEvent = createQueuedIpcListener( const onNotificationHistoryToggleEvent = createQueuedIpcListener(
IPC_CHANNELS.event.notificationHistoryToggle, IPC_CHANNELS.event.notificationHistoryToggle,
); );
const onSubtitleVisibilityEvent = createLatestValueIpcListenerWithPayload<boolean>(
IPC_CHANNELS.event.subtitleVisibility,
(payload) => payload === true,
);
const onSubtitlePositionSetEvent = createLatestValueIpcListenerWithPayload<SubtitlePosition | null>( const onSubtitlePositionSetEvent = createLatestValueIpcListenerWithPayload<SubtitlePosition | null>(
IPC_CHANNELS.event.subtitlePositionSet, IPC_CHANNELS.event.subtitlePositionSet,
(payload) => payload as SubtitlePosition | null, (payload) => payload as SubtitlePosition | null,
@@ -274,22 +266,14 @@ const electronAPI: ElectronAPI = {
}, },
onNotificationHistoryToggle: onNotificationHistoryToggleEvent, onNotificationHistoryToggle: onNotificationHistoryToggleEvent,
onVisibility: (callback: (visible: boolean) => void) => {
onSubtitleVisibilityEvent(callback);
},
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => { onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => {
onSubtitlePositionSetEvent(callback); onSubtitlePositionSetEvent(callback);
}, },
getOverlayVisibility: (): Promise<boolean> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getVisibleOverlayVisibility),
getCurrentSubtitle: (): Promise<SubtitleData> => getCurrentSubtitle: (): Promise<SubtitleData> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitle), ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitle),
getCurrentSubtitleRaw: (): Promise<string> => getCurrentSubtitleRaw: (): Promise<string> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleRaw), ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleRaw),
getCurrentSubtitleAss: (): Promise<string> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleAss),
getSubtitleSidebarOpen: (): Promise<boolean> => getSubtitleSidebarOpen: (): Promise<boolean> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getSubtitleSidebarOpen), ipcRenderer.invoke(IPC_CHANNELS.request.getSubtitleSidebarOpen),
getSubtitleSidebarSnapshot: () => getSubtitleSidebarSnapshot: () =>
@@ -302,14 +286,6 @@ const electronAPI: ElectronAPI = {
}, },
getPlaybackPaused: (): Promise<boolean | null> => getPlaybackPaused: (): Promise<boolean | null> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getPlaybackPaused), ipcRenderer.invoke(IPC_CHANNELS.request.getPlaybackPaused),
onSubtitleAss: (callback: (assText: string) => void) => {
ipcRenderer.on(
IPC_CHANNELS.event.subtitleAssSet,
(_event: IpcRendererEvent, assText: string) => {
callback(assText);
},
);
},
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => { setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
ipcRenderer.send(IPC_CHANNELS.command.setIgnoreMouseEvents, ignore, options); ipcRenderer.send(IPC_CHANNELS.command.setIgnoreMouseEvents, ignore, options);
@@ -319,10 +295,6 @@ const electronAPI: ElectronAPI = {
ipcRenderer.send(IPC_CHANNELS.command.reportOverlayInteractive, interactive); ipcRenderer.send(IPC_CHANNELS.command.reportOverlayInteractive, interactive);
}, },
openYomitanSettings: () => {
ipcRenderer.send(IPC_CHANNELS.command.openYomitanSettings);
},
recordYomitanLookup: (context?: SubtitleMiningContext | null) => { recordYomitanLookup: (context?: SubtitleMiningContext | null) => {
ipcRenderer.send(IPC_CHANNELS.command.recordYomitanLookup, context ?? null); ipcRenderer.send(IPC_CHANNELS.command.recordYomitanLookup, context ?? null);
}, },
@@ -333,18 +305,10 @@ const electronAPI: ElectronAPI = {
ipcRenderer.send(IPC_CHANNELS.command.saveSubtitlePosition, position); ipcRenderer.send(IPC_CHANNELS.command.saveSubtitlePosition, position);
}, },
getMecabStatus: (): Promise<MecabStatus> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getMecabStatus),
setMecabEnabled: (enabled: boolean) => {
ipcRenderer.send(IPC_CHANNELS.command.setMecabEnabled, enabled);
},
sendMpvCommand: (command: (string | number)[]) => { sendMpvCommand: (command: (string | number)[]) => {
ipcRenderer.send(IPC_CHANNELS.command.mpvCommand, command); ipcRenderer.send(IPC_CHANNELS.command.mpvCommand, command);
}, },
getKeybindings: (): Promise<Keybinding[]> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getKeybindings),
getMpvInputBindings: () => ipcRenderer.invoke(IPC_CHANNELS.request.getMpvInputBindings), getMpvInputBindings: () => ipcRenderer.invoke(IPC_CHANNELS.request.getMpvInputBindings),
getSessionBindings: () => ipcRenderer.invoke(IPC_CHANNELS.request.getSessionBindings), getSessionBindings: () => ipcRenderer.invoke(IPC_CHANNELS.request.getSessionBindings),
getConfiguredShortcuts: (): Promise<Required<ShortcutsConfig>> => getConfiguredShortcuts: (): Promise<Required<ShortcutsConfig>> =>
@@ -363,8 +327,6 @@ const electronAPI: ElectronAPI = {
ipcRenderer.invoke(IPC_CHANNELS.request.getControllerConfig), ipcRenderer.invoke(IPC_CHANNELS.request.getControllerConfig),
saveControllerConfig: (update: ControllerConfigUpdate): Promise<void> => saveControllerConfig: (update: ControllerConfigUpdate): Promise<void> =>
ipcRenderer.invoke(IPC_CHANNELS.command.saveControllerConfig, update), ipcRenderer.invoke(IPC_CHANNELS.command.saveControllerConfig, update),
saveControllerPreference: (update: ControllerPreferenceUpdate): Promise<void> =>
ipcRenderer.invoke(IPC_CHANNELS.command.saveControllerPreference, update),
getJimakuMediaInfo: (): Promise<JimakuMediaInfo> => getJimakuMediaInfo: (): Promise<JimakuMediaInfo> =>
ipcRenderer.invoke(IPC_CHANNELS.request.jimakuGetMediaInfo), ipcRenderer.invoke(IPC_CHANNELS.request.jimakuGetMediaInfo),
@@ -388,31 +350,14 @@ const electronAPI: ElectronAPI = {
tsukihimeGetSecondaryLanguages: (): Promise<string[]> => tsukihimeGetSecondaryLanguages: (): Promise<string[]> =>
ipcRenderer.invoke(IPC_CHANNELS.request.tsukihimeGetSecondaryLanguages), ipcRenderer.invoke(IPC_CHANNELS.request.tsukihimeGetSecondaryLanguages),
quitApp: () => {
ipcRenderer.send(IPC_CHANNELS.command.quitApp);
},
toggleDevTools: () => { toggleDevTools: () => {
ipcRenderer.send(IPC_CHANNELS.command.toggleDevTools); ipcRenderer.send(IPC_CHANNELS.command.toggleDevTools);
}, },
toggleOverlay: () => {
ipcRenderer.send(IPC_CHANNELS.command.toggleOverlay);
},
toggleStatsOverlay: () => { toggleStatsOverlay: () => {
ipcRenderer.send(IPC_CHANNELS.command.toggleStatsOverlay); ipcRenderer.send(IPC_CHANNELS.command.toggleStatsOverlay);
}, },
getAnkiConnectStatus: (): Promise<boolean> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getAnkiConnectStatus),
setAnkiConnectEnabled: (enabled: boolean) => {
ipcRenderer.send(IPC_CHANNELS.command.setAnkiConnectEnabled, enabled);
},
clearAnkiConnectHistory: () => {
ipcRenderer.send(IPC_CHANNELS.command.clearAnkiConnectHistory);
},
onSecondarySub: (callback: (text: string) => void) => { onSecondarySub: (callback: (text: string) => void) => {
onSecondarySubtitleSetEvent(callback); onSecondarySubtitleSetEvent(callback);
}, },
@@ -457,8 +402,6 @@ const electronAPI: ElectronAPI = {
value: RuntimeOptionValue, value: RuntimeOptionValue,
): Promise<RuntimeOptionApplyResult> => ): Promise<RuntimeOptionApplyResult> =>
ipcRenderer.invoke(IPC_CHANNELS.request.setRuntimeOption, id, value), ipcRenderer.invoke(IPC_CHANNELS.request.setRuntimeOption, id, value),
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1): Promise<RuntimeOptionApplyResult> =>
ipcRenderer.invoke(IPC_CHANNELS.request.cycleRuntimeOption, id, direction),
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => { onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => {
ipcRenderer.on( ipcRenderer.on(
IPC_CHANNELS.event.runtimeOptionsChanged, IPC_CHANNELS.event.runtimeOptionsChanged,
@@ -523,8 +466,6 @@ const electronAPI: ElectronAPI = {
onSessionNumericSelectionStart: onSessionNumericSelectionStartEvent, onSessionNumericSelectionStart: onSessionNumericSelectionStartEvent,
onKeyboardModeToggleRequested: onKeyboardModeToggleRequestedEvent, onKeyboardModeToggleRequested: onKeyboardModeToggleRequestedEvent,
onLookupWindowToggleRequested: onLookupWindowToggleRequestedEvent, onLookupWindowToggleRequested: onLookupWindowToggleRequestedEvent,
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue),
getPlaylistBrowserSnapshot: (): Promise<PlaylistBrowserSnapshot> => getPlaylistBrowserSnapshot: (): Promise<PlaylistBrowserSnapshot> =>
ipcRenderer.invoke(IPC_CHANNELS.request.getPlaylistBrowserSnapshot), ipcRenderer.invoke(IPC_CHANNELS.request.getPlaylistBrowserSnapshot),
appendPlaylistBrowserFile: (pathValue: string): Promise<PlaylistBrowserMutationResult> => appendPlaylistBrowserFile: (pathValue: string): Promise<PlaylistBrowserMutationResult> =>
+1 -2
View File
@@ -247,7 +247,6 @@ function installKeyboardTestGlobals() {
windowFocusCalls += 1; windowFocusCalls += 1;
}, },
electronAPI: { electronAPI: {
getKeybindings: async () => [],
getMpvInputBindings: () => getMpvInputBindings(), getMpvInputBindings: () => getMpvInputBindings(),
getSessionBindings: () => getSessionBindingsImpl(), getSessionBindings: () => getSessionBindingsImpl(),
getConfiguredShortcuts: async () => configuredShortcuts, getConfiguredShortcuts: async () => configuredShortcuts,
@@ -1590,7 +1589,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 () => { 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 { try {
await handlers.setupMpvInputForwarding(); await handlers.setupMpvInputForwarding();
@@ -30,12 +30,6 @@ export function createYoutubeTrackPickerModal(
ctx.dom.youtubePickerStatus.style.color = isError ? '#ed8796' : '#a5adcb'; 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 { function renderTrackList(): void {
ctx.dom.youtubePickerTracks.replaceChildren(); ctx.dom.youtubePickerTracks.replaceChildren();
const payload = ctx.state.youtubePickerPayload; const payload = ctx.state.youtubePickerPayload;
-21
View File
@@ -25,22 +25,15 @@ export const IPC_CHANNELS = {
command: { command: {
setIgnoreMouseEvents: 'set-ignore-mouse-events', setIgnoreMouseEvents: 'set-ignore-mouse-events',
overlayModalClosed: 'overlay:modal-closed', overlayModalClosed: 'overlay:modal-closed',
openYomitanSettings: 'open-yomitan-settings',
recordYomitanLookup: 'record-yomitan-lookup', recordYomitanLookup: 'record-yomitan-lookup',
quitApp: 'quit-app',
youtubePickerResolve: 'youtube:picker-resolve', youtubePickerResolve: 'youtube:picker-resolve',
toggleDevTools: 'toggle-dev-tools', toggleDevTools: 'toggle-dev-tools',
toggleOverlay: 'toggle-overlay',
saveSubtitlePosition: 'save-subtitle-position', saveSubtitlePosition: 'save-subtitle-position',
statsNativeConfirmDialog: 'stats:native-confirm-dialog', statsNativeConfirmDialog: 'stats:native-confirm-dialog',
statsNativeDialogOpened: 'stats:native-dialog-opened', statsNativeDialogOpened: 'stats:native-dialog-opened',
statsNativeDialogClosed: 'stats:native-dialog-closed', statsNativeDialogClosed: 'stats:native-dialog-closed',
saveControllerConfig: 'save-controller-config', saveControllerConfig: 'save-controller-config',
saveControllerPreference: 'save-controller-preference',
setMecabEnabled: 'set-mecab-enabled',
mpvCommand: 'mpv-command', mpvCommand: 'mpv-command',
setAnkiConnectEnabled: 'set-anki-connect-enabled',
clearAnkiConnectHistory: 'clear-anki-connect-history',
refreshKnownWords: 'anki:refresh-known-words', refreshKnownWords: 'anki:refresh-known-words',
kikuFieldGroupingRespond: 'kiku:field-grouping-respond', kikuFieldGroupingRespond: 'kiku:field-grouping-respond',
reportOverlayContentBounds: 'overlay-content-bounds:report', reportOverlayContentBounds: 'overlay-content-bounds:report',
@@ -63,18 +56,14 @@ export const IPC_CHANNELS = {
downloadSubtitleGenerationVadModel: 'subtitle-generation:download-vad', downloadSubtitleGenerationVadModel: 'subtitle-generation:download-vad',
setSubtitleGenerationVadEnabled: 'subtitle-generation:set-vad-enabled', setSubtitleGenerationVadEnabled: 'subtitle-generation:set-vad-enabled',
cancelSubtitleGeneration: 'subtitle-generation:cancel', cancelSubtitleGeneration: 'subtitle-generation:cancel',
getVisibleOverlayVisibility: 'get-visible-overlay-visibility',
getCurrentSubtitle: 'get-current-subtitle', getCurrentSubtitle: 'get-current-subtitle',
getCurrentSubtitleRaw: 'get-current-subtitle-raw', getCurrentSubtitleRaw: 'get-current-subtitle-raw',
getCurrentSubtitleAss: 'get-current-subtitle-ass',
getSubtitleSidebarSnapshot: 'get-subtitle-sidebar-snapshot', getSubtitleSidebarSnapshot: 'get-subtitle-sidebar-snapshot',
getSubtitleSidebarPlaybackState: 'get-subtitle-sidebar-playback-state', getSubtitleSidebarPlaybackState: 'get-subtitle-sidebar-playback-state',
getSubtitleSidebarOpen: 'get-subtitle-sidebar-open', getSubtitleSidebarOpen: 'get-subtitle-sidebar-open',
getPlaybackPaused: 'get-playback-paused', getPlaybackPaused: 'get-playback-paused',
getSubtitlePosition: 'get-subtitle-position', getSubtitlePosition: 'get-subtitle-position',
getSubtitleStyle: 'get-subtitle-style', getSubtitleStyle: 'get-subtitle-style',
getMecabStatus: 'get-mecab-status',
getKeybindings: 'get-keybindings',
getMpvInputBindings: 'get-mpv-input-bindings', getMpvInputBindings: 'get-mpv-input-bindings',
getSessionBindings: 'get-session-bindings', getSessionBindings: 'get-session-bindings',
getConfigShortcuts: 'get-config-shortcuts', getConfigShortcuts: 'get-config-shortcuts',
@@ -88,10 +77,8 @@ export const IPC_CHANNELS = {
focusMainWindow: 'focus-main-window', focusMainWindow: 'focus-main-window',
activatePlaybackWindowForOverlayInteraction: 'overlay:activate-playback-window', activatePlaybackWindowForOverlayInteraction: 'overlay:activate-playback-window',
runSubsyncManual: 'subsync:run-manual', runSubsyncManual: 'subsync:run-manual',
getAnkiConnectStatus: 'get-anki-connect-status',
getRuntimeOptions: 'runtime-options:get', getRuntimeOptions: 'runtime-options:get',
setRuntimeOption: 'runtime-options:set', setRuntimeOption: 'runtime-options:set',
cycleRuntimeOption: 'runtime-options:cycle',
getAnilistStatus: 'anilist:get-status', getAnilistStatus: 'anilist:get-status',
clearAnilistToken: 'anilist:clear-token', clearAnilistToken: 'anilist:clear-token',
openAnilistSetup: 'anilist:open-setup', openAnilistSetup: 'anilist:open-setup',
@@ -103,7 +90,6 @@ export const IPC_CHANNELS = {
getCharacterDictionaryManagerSnapshot: 'character-dictionary:get-manager-snapshot', getCharacterDictionaryManagerSnapshot: 'character-dictionary:get-manager-snapshot',
removeCharacterDictionaryManagedEntry: 'character-dictionary:remove-managed-entry', removeCharacterDictionaryManagedEntry: 'character-dictionary:remove-managed-entry',
moveCharacterDictionaryManagedEntry: 'character-dictionary:move-managed-entry', moveCharacterDictionaryManagedEntry: 'character-dictionary:move-managed-entry',
appendClipboardVideoToQueue: 'clipboard:append-video-to-queue',
getPlaylistBrowserSnapshot: 'playlist-browser:get-snapshot', getPlaylistBrowserSnapshot: 'playlist-browser:get-snapshot',
appendPlaylistBrowserFile: 'playlist-browser:append-file', appendPlaylistBrowserFile: 'playlist-browser:append-file',
playPlaylistBrowserIndex: 'playlist-browser:play-index', playPlaylistBrowserIndex: 'playlist-browser:play-index',
@@ -153,9 +139,7 @@ export const IPC_CHANNELS = {
subtitleGenerationProgress: 'subtitle-generation:progress', subtitleGenerationProgress: 'subtitle-generation:progress',
subtitleSet: 'subtitle:set', subtitleSet: 'subtitle:set',
overlayPointerRecoveryRequest: 'overlay:pointer-recovery-request', overlayPointerRecoveryRequest: 'overlay:pointer-recovery-request',
subtitleVisibility: 'mpv:subVisibility',
subtitlePositionSet: 'subtitle-position:set', subtitlePositionSet: 'subtitle-position:set',
subtitleAssSet: 'subtitle-ass:set',
secondarySubtitleSet: 'secondary-subtitle:set', secondarySubtitleSet: 'secondary-subtitle:set',
secondarySubtitleMode: 'secondary-subtitle:mode', secondarySubtitleMode: 'secondary-subtitle:mode',
subsyncOpenManual: 'subsync:open-manual', subsyncOpenManual: 'subsync:open-manual',
@@ -197,11 +181,6 @@ export type RuntimeOptionsSetRequest = {
value: RuntimeOptionValue; value: RuntimeOptionValue;
}; };
export type RuntimeOptionsCycleRequest = {
id: RuntimeOptionId;
direction: 1 | -1;
};
export type OverlayContentBoundsReportRequest = { export type OverlayContentBoundsReportRequest = {
measurement: OverlayContentMeasurement; measurement: OverlayContentMeasurement;
}; };
-15
View File
@@ -10,7 +10,6 @@ import type {
} from '../../types/integrations'; } from '../../types/integrations';
import type { import type {
ControllerConfigUpdate, ControllerConfigUpdate,
ControllerPreferenceUpdate,
SessionActionDispatchRequest, SessionActionDispatchRequest,
SubsyncManualRunRequest, SubsyncManualRunRequest,
} from '../../types/runtime'; } from '../../types/runtime';
@@ -131,16 +130,6 @@ export function parseSubtitlePosition(value: unknown): SubtitlePosition | null {
}; };
} }
export function parseControllerPreferenceUpdate(value: unknown): ControllerPreferenceUpdate | null {
if (!isObject(value)) return null;
if (typeof value.preferredGamepadId !== 'string') return null;
if (typeof value.preferredGamepadLabel !== 'string') return null;
return {
preferredGamepadId: value.preferredGamepadId,
preferredGamepadLabel: value.preferredGamepadLabel,
};
}
function parseDiscreteBinding(value: unknown) { function parseDiscreteBinding(value: unknown) {
if (!isObject(value) || typeof value.kind !== 'string') return null; if (!isObject(value) || typeof value.kind !== 'string') return null;
if (value.kind === 'none') { if (value.kind === 'none') {
@@ -328,10 +317,6 @@ export function parseRuntimeOptionId(value: unknown): RuntimeOptionId | null {
return RUNTIME_OPTION_IDS.includes(value as RuntimeOptionId) ? (value as RuntimeOptionId) : null; return RUNTIME_OPTION_IDS.includes(value as RuntimeOptionId) ? (value as RuntimeOptionId) : null;
} }
export function parseRuntimeOptionDirection(value: unknown): 1 | -1 | null {
return value === 1 || value === -1 ? value : null;
}
export function parseRuntimeOptionValue(value: unknown): RuntimeOptionValue | null { export function parseRuntimeOptionValue(value: unknown): RuntimeOptionValue | null {
return typeof value === 'boolean' || typeof value === 'string' return typeof value === 'boolean' || typeof value === 'string'
? (value as RuntimeOptionValue) ? (value as RuntimeOptionValue)
-3
View File
@@ -4,7 +4,6 @@ import type {
AiConfig, AiConfig,
AiFeatureConfig, AiFeatureConfig,
AnilistCharacterDictionaryCollapsibleSectionsConfig, AnilistCharacterDictionaryCollapsibleSectionsConfig,
AnilistCharacterDictionaryEvictionPolicy,
AnilistCharacterDictionaryProfileScope, AnilistCharacterDictionaryProfileScope,
AnilistConfig, AnilistConfig,
DiscordPresenceConfig, DiscordPresenceConfig,
@@ -349,9 +348,7 @@ export interface ResolvedConfig {
enabled: boolean; enabled: boolean;
accessToken: string; accessToken: string;
characterDictionary: { characterDictionary: {
refreshTtlHours: number;
maxLoaded: number; maxLoaded: number;
evictionPolicy: AnilistCharacterDictionaryEvictionPolicy;
profileScope: AnilistCharacterDictionaryProfileScope; profileScope: AnilistCharacterDictionaryProfileScope;
collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>; collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>;
}; };
-3
View File
@@ -50,7 +50,6 @@ export interface JimakuConfig {
maxEntryResults?: number; maxEntryResults?: number;
} }
export type AnilistCharacterDictionaryEvictionPolicy = 'disable' | 'delete';
export type AnilistCharacterDictionaryProfileScope = 'all' | 'active'; export type AnilistCharacterDictionaryProfileScope = 'all' | 'active';
export type AnilistCharacterDictionaryCollapsibleSectionKey = export type AnilistCharacterDictionaryCollapsibleSectionKey =
| 'description' | 'description'
@@ -64,9 +63,7 @@ export interface AnilistCharacterDictionaryCollapsibleSectionsConfig {
} }
export interface AnilistCharacterDictionaryConfig { export interface AnilistCharacterDictionaryConfig {
refreshTtlHours?: number;
maxLoaded?: number; maxLoaded?: number;
evictionPolicy?: AnilistCharacterDictionaryEvictionPolicy;
profileScope?: AnilistCharacterDictionaryProfileScope; profileScope?: AnilistCharacterDictionaryProfileScope;
collapsibleSections?: AnilistCharacterDictionaryCollapsibleSectionsConfig; collapsibleSections?: AnilistCharacterDictionaryCollapsibleSectionsConfig;
} }
-22
View File
@@ -58,7 +58,6 @@ import type {
SubtitleSidebarPlaybackState, SubtitleSidebarPlaybackState,
SubtitleSidebarSnapshot, SubtitleSidebarSnapshot,
SubtitleRendererStyleConfig, SubtitleRendererStyleConfig,
SubtitleStyleConfig,
} from './subtitle'; } from './subtitle';
import type { import type {
RuntimeOptionApplyResult, RuntimeOptionApplyResult,
@@ -298,11 +297,6 @@ export interface ControllerConfig {
profiles?: Record<string, ControllerProfileConfig>; profiles?: Record<string, ControllerProfileConfig>;
} }
export interface ControllerPreferenceUpdate {
preferredGamepadId: string;
preferredGamepadLabel: string;
}
export type ControllerConfigUpdate = ControllerConfig; export type ControllerConfigUpdate = ControllerConfig;
export interface ControllerDeviceInfo { export interface ControllerDeviceInfo {
@@ -471,28 +465,20 @@ export interface ElectronAPI {
options?: Pick<OverlayNotificationAction, 'noteId'>, options?: Pick<OverlayNotificationAction, 'noteId'>,
) => void; ) => void;
onNotificationHistoryToggle: (callback: () => void) => void; onNotificationHistoryToggle: (callback: () => void) => void;
onVisibility: (callback: (visible: boolean) => void) => void;
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void; onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
getOverlayVisibility: () => Promise<boolean>;
getCurrentSubtitle: () => Promise<SubtitleData>; getCurrentSubtitle: () => Promise<SubtitleData>;
getCurrentSubtitleRaw: () => Promise<string>; getCurrentSubtitleRaw: () => Promise<string>;
getCurrentSubtitleAss: () => Promise<string>;
getSubtitleSidebarSnapshot: () => Promise<SubtitleSidebarSnapshot>; getSubtitleSidebarSnapshot: () => Promise<SubtitleSidebarSnapshot>;
getSubtitleSidebarPlaybackState: () => Promise<SubtitleSidebarPlaybackState>; getSubtitleSidebarPlaybackState: () => Promise<SubtitleSidebarPlaybackState>;
copySubtitleSidebarSelection: (text: string) => Promise<void>; copySubtitleSidebarSelection: (text: string) => Promise<void>;
getSubtitleSidebarOpen: () => Promise<boolean>; getSubtitleSidebarOpen: () => Promise<boolean>;
getPlaybackPaused: () => Promise<boolean | null>; getPlaybackPaused: () => Promise<boolean | null>;
onSubtitleAss: (callback: (assText: string) => void) => void;
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void; setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
reportOverlayInteractive: (interactive: boolean) => void; reportOverlayInteractive: (interactive: boolean) => void;
openYomitanSettings: () => void;
recordYomitanLookup: (context?: SubtitleMiningContext | null) => void; recordYomitanLookup: (context?: SubtitleMiningContext | null) => void;
getSubtitlePosition: () => Promise<SubtitlePosition | null>; getSubtitlePosition: () => Promise<SubtitlePosition | null>;
saveSubtitlePosition: (position: SubtitlePosition) => void; saveSubtitlePosition: (position: SubtitlePosition) => void;
getMecabStatus: () => Promise<MecabStatus>;
setMecabEnabled: (enabled: boolean) => void;
sendMpvCommand: (command: (string | number)[]) => void; sendMpvCommand: (command: (string | number)[]) => void;
getKeybindings: () => Promise<Keybinding[]>;
getMpvInputBindings: () => Promise<MpvInputBindingsSnapshot>; getMpvInputBindings: () => Promise<MpvInputBindingsSnapshot>;
getSessionBindings: () => Promise<CompiledSessionBinding[]>; getSessionBindings: () => Promise<CompiledSessionBinding[]>;
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>; getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
@@ -506,7 +492,6 @@ export interface ElectronAPI {
markActiveVideoWatched: () => Promise<boolean>; markActiveVideoWatched: () => Promise<boolean>;
getControllerConfig: () => Promise<ResolvedControllerConfig>; getControllerConfig: () => Promise<ResolvedControllerConfig>;
saveControllerConfig: (update: ControllerConfigUpdate) => Promise<void>; saveControllerConfig: (update: ControllerConfigUpdate) => Promise<void>;
saveControllerPreference: (update: ControllerPreferenceUpdate) => Promise<void>;
getJimakuMediaInfo: () => Promise<JimakuMediaInfo>; getJimakuMediaInfo: () => Promise<JimakuMediaInfo>;
jimakuSearchEntries: (query: JimakuSearchQuery) => Promise<JimakuApiResponse<JimakuEntry[]>>; jimakuSearchEntries: (query: JimakuSearchQuery) => Promise<JimakuApiResponse<JimakuEntry[]>>;
jimakuListFiles: (query: JimakuFilesQuery) => Promise<JimakuApiResponse<JimakuFileEntry[]>>; jimakuListFiles: (query: JimakuFilesQuery) => Promise<JimakuApiResponse<JimakuFileEntry[]>>;
@@ -519,13 +504,8 @@ export interface ElectronAPI {
) => Promise<TsukihimeApiResponse<TsukihimeSubtitleFile[]>>; ) => Promise<TsukihimeApiResponse<TsukihimeSubtitleFile[]>>;
tsukihimeDownloadFile: (query: TsukihimeDownloadQuery) => Promise<TsukihimeDownloadResult>; tsukihimeDownloadFile: (query: TsukihimeDownloadQuery) => Promise<TsukihimeDownloadResult>;
tsukihimeGetSecondaryLanguages: () => Promise<string[]>; tsukihimeGetSecondaryLanguages: () => Promise<string[]>;
quitApp: () => void;
toggleDevTools: () => void; toggleDevTools: () => void;
toggleOverlay: () => void;
toggleStatsOverlay: () => void; toggleStatsOverlay: () => void;
getAnkiConnectStatus: () => Promise<boolean>;
setAnkiConnectEnabled: (enabled: boolean) => void;
clearAnkiConnectHistory: () => void;
onSecondarySub: (callback: (text: string) => void) => void; onSecondarySub: (callback: (text: string) => void) => void;
onSecondarySubMode: (callback: (mode: SecondarySubMode) => void) => void; onSecondarySubMode: (callback: (mode: SecondarySubMode) => void) => void;
getSecondarySubMode: () => Promise<SecondarySubMode>; getSecondarySubMode: () => Promise<SecondarySubMode>;
@@ -551,7 +531,6 @@ export interface ElectronAPI {
id: RuntimeOptionId, id: RuntimeOptionId,
value: RuntimeOptionValue, value: RuntimeOptionValue,
) => Promise<RuntimeOptionApplyResult>; ) => Promise<RuntimeOptionApplyResult>;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => Promise<RuntimeOptionApplyResult>;
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void; onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
onOpenRuntimeOptions: (callback: () => void) => void; onOpenRuntimeOptions: (callback: () => void) => void;
onOpenSessionHelp: (callback: (payload: SessionHelpOpenPayload) => void) => void; onOpenSessionHelp: (callback: (payload: SessionHelpOpenPayload) => void) => void;
@@ -590,7 +569,6 @@ export interface ElectronAPI {
) => void; ) => void;
onKeyboardModeToggleRequested: (callback: () => void) => void; onKeyboardModeToggleRequested: (callback: () => void) => void;
onLookupWindowToggleRequested: (callback: () => void) => void; onLookupWindowToggleRequested: (callback: () => void) => void;
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
getPlaylistBrowserSnapshot: () => Promise<PlaylistBrowserSnapshot>; getPlaylistBrowserSnapshot: () => Promise<PlaylistBrowserSnapshot>;
appendPlaylistBrowserFile: (path: string) => Promise<PlaylistBrowserMutationResult>; appendPlaylistBrowserFile: (path: string) => Promise<PlaylistBrowserMutationResult>;
playPlaylistBrowserIndex: (index: number) => Promise<PlaylistBrowserMutationResult>; playPlaylistBrowserIndex: (index: number) => Promise<PlaylistBrowserMutationResult>;
+2 -1
View File
@@ -13,7 +13,8 @@ const RECT = koffi.struct('RECT', {
Bottom: 'int', Bottom: 'int',
}); });
const MARGINS = koffi.struct('MARGINS', { // Registered by name for the DwmExtendFrameIntoClientArea prototype below.
koffi.struct('MARGINS', {
cxLeftWidth: 'int', cxLeftWidth: 'int',
cxRightWidth: 'int', cxRightWidth: 'int',
cyTopHeight: 'int', cyTopHeight: 'int',
+1
View File
@@ -3,6 +3,7 @@
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"noEmit": true, "noEmit": true,
"noUnusedLocals": true,
"declaration": false, "declaration": false,
"declarationMap": false, "declarationMap": false,
"sourceMap": false "sourceMap": false