├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .tauri-updater.json ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── icon.png ├── svelte.svg ├── tauri.svg └── vite.svg ├── script ├── all_games.json ├── package-lock.json ├── package.json └── scrape_sql.cjs ├── src-tauri ├── .gitignore ├── .vscode │ └── settings.json ├── Cargo.lock ├── Cargo.toml ├── bin │ └── extract-icon-x86_64-pc-windows-msvc.exe ├── build.rs ├── capabilities │ └── migrated.json ├── gen │ └── schemas │ │ ├── acl-manifests.json │ │ ├── capabilities.json │ │ ├── desktop-schema.json │ │ └── windows-schema.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── notfound.png ├── src │ ├── domain │ │ ├── all_game_cache.rs │ │ ├── collection.rs │ │ ├── distance.rs │ │ ├── explored_cache.rs │ │ ├── explorer │ │ │ ├── file.rs │ │ │ ├── mod.rs │ │ │ └── network.rs │ │ ├── file.rs │ │ ├── mod.rs │ │ ├── network.rs │ │ ├── process.rs │ │ ├── repository │ │ │ ├── all_game_cache.rs │ │ │ ├── collection.rs │ │ │ ├── explored_cache.rs │ │ │ └── mod.rs │ │ └── windows │ │ │ ├── mod.rs │ │ │ └── process.rs │ ├── infrastructure │ │ ├── explorerimpl │ │ │ ├── explorer.rs │ │ │ ├── file.rs │ │ │ ├── mod.rs │ │ │ └── network.rs │ │ ├── mod.rs │ │ ├── repositoryimpl │ │ │ ├── all_game_cahe.rs │ │ │ ├── collection.rs │ │ │ ├── driver.rs │ │ │ ├── explored_cache.rs │ │ │ ├── mod.rs │ │ │ ├── models │ │ │ │ ├── all_game_cache.rs │ │ │ │ ├── collection.rs │ │ │ │ └── mod.rs │ │ │ └── repository.rs │ │ ├── util.rs │ │ └── windowsimpl │ │ │ ├── mod.rs │ │ │ ├── process.rs │ │ │ ├── screenshot │ │ │ ├── README.md │ │ │ ├── capture.rs │ │ │ ├── d3d.rs │ │ │ ├── mod.rs │ │ │ └── take.rs │ │ │ └── windows.rs │ ├── interface │ │ ├── command.rs │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── models │ │ │ ├── all_game_cache.rs │ │ │ ├── collection.rs │ │ │ └── mod.rs │ │ └── module.rs │ ├── main.rs │ ├── migrations │ │ ├── V1__init.sql │ │ └── V2__thumbnail_size.sql │ └── usecase │ │ ├── all_game_cache.rs │ │ ├── collection.rs │ │ ├── error.rs │ │ ├── explored_cache.rs │ │ ├── file.rs │ │ ├── file_test.rs │ │ ├── mod.rs │ │ ├── models │ │ ├── collection.rs │ │ ├── file.rs │ │ └── mod.rs │ │ ├── network.rs │ │ └── process.rs └── tauri.conf.json ├── src ├── App.svelte ├── components │ ├── Home │ │ ├── ImportDropFiles.svelte │ │ └── ZappingGameItem.svelte │ ├── Sidebar │ │ ├── CollectionElement.svelte │ │ ├── CollectionElements.svelte │ │ ├── Header.svelte │ │ ├── ImportAutomatically.svelte │ │ ├── ImportManually.svelte │ │ ├── ImportPopover.svelte │ │ ├── MinimalSidebar.svelte │ │ ├── Search.svelte │ │ ├── SearchAttribute.svelte │ │ ├── SearchAttrributeControl.svelte │ │ ├── SearchInput.svelte │ │ ├── Sidebar.svelte │ │ ├── SortPopover.svelte │ │ ├── SubHeader.svelte │ │ ├── search.ts │ │ ├── searchAttributes.ts │ │ └── sort.ts │ ├── Tab │ │ ├── ATab.svelte │ │ └── ATabList.svelte │ ├── UI │ │ ├── ADisclosure.svelte │ │ ├── APopover.svelte │ │ ├── Button.svelte │ │ ├── ButtonBase.svelte │ │ ├── ButtonCancel.svelte │ │ ├── ButtonIcon.svelte │ │ ├── Checkbox.svelte │ │ ├── IconButton.svelte │ │ ├── Input.svelte │ │ ├── InputPath.svelte │ │ ├── LinkButton.svelte │ │ ├── LinkText.svelte │ │ ├── Modal.svelte │ │ ├── ModalBase.svelte │ │ ├── OptionButton.svelte │ │ ├── QRCodeCanvas.svelte │ │ ├── ScrollableHorizontal.svelte │ │ ├── Select.svelte │ │ ├── SelectOptions.svelte │ │ ├── Table.svelte │ │ ├── VirtualScroller.svelte │ │ ├── VirtualScrollerMasonry.svelte │ │ ├── button.ts │ │ ├── virtualScroller.ts │ │ └── virtualScrollerMasonry.ts │ └── Work │ │ ├── Actions.svelte │ │ ├── DeleteElement.svelte │ │ ├── Detail.svelte │ │ ├── DetailRow.svelte │ │ ├── LinkToSidebar.svelte │ │ ├── OtherInfomationSection.svelte │ │ ├── OtherInformation.svelte │ │ ├── PlayButton.svelte │ │ ├── PlayPopover.svelte │ │ ├── QRCode.svelte │ │ ├── SettingPopover.svelte │ │ ├── Work.svelte │ │ ├── WorkImage.svelte │ │ ├── WorkLayout.svelte │ │ └── WorkMain.svelte ├── index.scss ├── layouts │ └── Layout.svelte ├── lib │ ├── chunk.ts │ ├── command.ts │ ├── filter.ts │ ├── importManually.ts │ ├── registerCollectionElementDetails.ts │ ├── scrapeAllGame.ts │ ├── scrapeSeiya.ts │ ├── scrapeSql.ts │ ├── scrapeWork.ts │ ├── toast.ts │ ├── trieFilter.ts │ ├── types.ts │ └── utils.ts ├── main.ts ├── router │ └── route.ts ├── store │ ├── memo.ts │ ├── query.ts │ ├── seiya.ts │ ├── showSidebar.ts │ ├── sidebarCollectionElements.ts │ ├── skyway.ts │ ├── startProcessMap.ts │ ├── tabs.ts │ └── works.ts ├── toast.scss ├── views │ ├── Home.svelte │ ├── Memo.svelte │ └── Work.svelte └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── unocss.config.ts ├── update.ps1 └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | script 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { 5 | sourceType: "module", 6 | ecmaVersion: 2019, 7 | project: "./tsconfig.json", 8 | extraFileExtensions: [".svelte"], 9 | }, 10 | extends: [ 11 | // add more generic rule sets here, such as: 12 | // 'eslint:recommended', 13 | "plugin:svelte/recommended", 14 | ], 15 | rules: { 16 | // override/add rules settings here, such as: 17 | // 'svelte/rule-name': 'error' 18 | }, 19 | overrides: [ 20 | { 21 | files: ["*.svelte"], 22 | parser: "svelte-eslint-parser", 23 | // Parse the ` 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "launcherg", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json", 11 | "lint": "eslint --ext .js,.ts,.svelte,.cjs src --fix", 12 | "tauri": "tauri" 13 | }, 14 | "dependencies": { 15 | "@skyway-sdk/room": "^1.5.4", 16 | "@tauri-apps/api": "^2.0.0-rc.1", 17 | "@tauri-apps/plugin-clipboard-manager": "^2.1.0-beta.6", 18 | "@tauri-apps/plugin-dialog": "^2.0.0-rc.0", 19 | "@tauri-apps/plugin-fs": "^2.0.0-rc.1", 20 | "@tauri-apps/plugin-http": "^2.0.0-rc.1", 21 | "@tauri-apps/plugin-shell": "^2.0.0-rc.0", 22 | "@unocss/reset": "^0.52.5", 23 | "easymde": "^2.18.0", 24 | "encoding-japanese": "^2.0.0", 25 | "qrious": "^4.0.2", 26 | "simplebar": "^6.2.5", 27 | "svelte-spa-router": "^3.3.0", 28 | "tippy.js": "^6.3.7", 29 | "toastify-js": "^1.12.0", 30 | "trie-search": "^1.4.2", 31 | "wanakana": "^5.1.0" 32 | }, 33 | "devDependencies": { 34 | "@iconify-json/iconoir": "^1.1.28", 35 | "@iconify-json/material-symbols": "^1.1.44", 36 | "@rgossiaux/svelte-headlessui": "^1.0.2", 37 | "@sveltejs/vite-plugin-svelte": "^2.0.0", 38 | "@tauri-apps/cli": "^2.0.0-beta.21", 39 | "@tsconfig/svelte": "^4.0.0", 40 | "@types/encoding-japanese": "^2.0.1", 41 | "@types/node": "^18.7.10", 42 | "@types/toastify-js": "^1.11.1", 43 | "@types/wanakana": "^4.0.3", 44 | "@typescript-eslint/parser": "^5.59.8", 45 | "@unocss/eslint-config": "^0.52.5", 46 | "@unocss/extractor-svelte": "^0.52.5", 47 | "@unocss/preset-attributify": "^0.52.5", 48 | "@unocss/preset-icons": "^0.52.5", 49 | "@unocss/preset-web-fonts": "^0.52.5", 50 | "@unocss/preset-wind": "^0.52.5", 51 | "@unocss/transformer-variant-group": "^0.52.5", 52 | "autoprefixer": "^10.4.14", 53 | "eslint": "^8.41.0", 54 | "eslint-plugin-svelte": "^2.29.0", 55 | "sass": "^1.62.1", 56 | "svelte": "^3.54.0", 57 | "svelte-check": "^3.0.0", 58 | "svelte-preprocess": "^5.0.0", 59 | "tslib": "^2.4.1", 60 | "typescript": "^5.0.0", 61 | "unocss": "^0.52.5", 62 | "vite": "^4.2.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/public/icon.png -------------------------------------------------------------------------------- /public/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "node-html-parser": "^6.1.5" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /script/scrape_sql.cjs: -------------------------------------------------------------------------------- 1 | const STEP = 5000; 2 | const MAX_SCRAPE_COUNT = 20; 3 | 4 | const fs = require("fs"); 5 | const { parse } = require("node-html-parser"); 6 | 7 | const scrape = async (idCursor) => { 8 | try { 9 | const formData = new FormData(); 10 | if (typeof idCursor !== "number" || isNaN(idCursor)) { 11 | return []; 12 | } 13 | const query = `SELECT id, gamename FROM gamelist WHERE id >= ${idCursor} AND id < ${ 14 | idCursor + STEP 15 | };`; 16 | formData.append("sql", query); 17 | const res = await fetch( 18 | "https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/sql_for_erogamer_form.php", 19 | { 20 | method: "POST", 21 | body: formData, 22 | } 23 | ); 24 | const text = await res.text(); 25 | const dom = parse(text); 26 | 27 | const games = []; 28 | dom.querySelectorAll("#query_result_main tr").forEach((tr, i) => { 29 | if (i === 0) { 30 | return; 31 | } 32 | const id = tr.querySelector("td:nth-child(1)"); 33 | const gamename = tr.querySelector("td:nth-child(2)"); 34 | if (!id || !gamename) return; 35 | games.push({ 36 | id: +id.innerHTML, 37 | gamename: gamename.innerHTML, 38 | }); 39 | }); 40 | return games; 41 | } catch (e) { 42 | console.error(e); 43 | return []; 44 | } 45 | }; 46 | 47 | const save = (data) => { 48 | const jsonData = JSON.stringify(data); 49 | 50 | fs.writeFile("all_games.json", jsonData, "utf8", (err) => { 51 | if (err) { 52 | console.error("An error occurred while writing to the file:", err); 53 | return; 54 | } 55 | console.log("The file has been saved!"); 56 | }); 57 | }; 58 | 59 | const execute = async () => { 60 | let idCursor = 0; 61 | const games = []; 62 | for (let i = 0; i < MAX_SCRAPE_COUNT; i++) { 63 | const appendGames = await scrape(idCursor); 64 | if (!appendGames.length) { 65 | console.log(`end within ${i + 1} loop. games.length: ${games.length}`); 66 | break; 67 | } 68 | games.push(...appendGames); 69 | await new Promise((resolve) => setTimeout(resolve, 5000)); 70 | idCursor += STEP; 71 | } 72 | 73 | save(games); 74 | }; 75 | 76 | execute(); 77 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | ".\\Cargo.toml", 4 | ".\\Cargo.toml" 5 | ], 6 | "rust-analyzer.showUnlinkedFileNotification": false 7 | } -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "launcherg" 3 | version = "0.0.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | tauri-build = { version = "2.0.0-beta", features = [] } 14 | 15 | [dependencies] 16 | tauri = { version = "2.0.0-beta", features = ["protocol-asset"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | anyhow = "1.0" 20 | thiserror = "1.0" 21 | async-trait = "0.1.57" 22 | sqlx = { version = "0.6", features = [ 23 | "runtime-tokio-rustls", 24 | "sqlite", 25 | "chrono", 26 | ] } 27 | chrono = { version = "0.4.26", features = ["serde"] } 28 | derive-new = "0.5.0" 29 | walkdir = "2" 30 | reqwest = { version = "0.11", features = ["json"] } 31 | futures = "0.3" 32 | image = "0.24.6" 33 | base64 = "0.21.2" 34 | dirs = "5.0.1" 35 | fast_image_resize = "3.0.4" 36 | url = "2.4.1" 37 | ico = "0.3.0" 38 | sysinfo = "0.29.10" 39 | refinery = { version = "0.8.9", features = ["rusqlite"] } 40 | axum = "0.7.5" 41 | tokio = { version = "1.0", features = ["net", "signal"] } 42 | tokio-util = "0.7.11" 43 | tauri-plugin-log = "2.0.0-rc.0" 44 | tauri-plugin-clipboard-manager = "2.1.0-beta.6" 45 | tauri-plugin-fs = "2.0.0-rc.0" 46 | tauri-plugin-dialog = "2.0.0-rc.0" 47 | tauri-plugin-shell = "2.0.0-rc.0" 48 | tauri-plugin-http = "2.0.0-rc.0" 49 | 50 | [dependencies.windows] 51 | version = "0.51" 52 | features = [ 53 | "Win32_System_Com", 54 | "Win32_Foundation", 55 | "Win32_System_Ole", 56 | "Win32_UI_Shell", 57 | "Win32_Storage_FileSystem", 58 | "Win32_UI_WindowsAndMessaging", 59 | "Win32_Graphics_Direct3D", 60 | "Graphics_Capture", 61 | "Win32_Graphics_Dxgi", 62 | "Win32_Graphics_Direct3D11", 63 | "Win32_System_WinRT_Direct3D11", 64 | "Graphics_DirectX_Direct3D11", 65 | "Win32_System_WinRT_Graphics_Capture", 66 | "Win32_Graphics_Dwm", 67 | "Win32_Graphics_Dxgi_Common", 68 | "Foundation", 69 | "Graphics_Imaging", 70 | "Storage", 71 | "Storage_Streams", 72 | ] 73 | 74 | [dependencies.uuid] 75 | version = "1.3.3" 76 | features = [ 77 | "v4", # Lets you generate random UUIDs 78 | "fast-rng", # Use a faster (but still sufficiently random) RNG 79 | "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs 80 | ] 81 | 82 | [features] 83 | # this feature is used for production builds or when `devPath` points to the filesystem 84 | # DO NOT REMOVE!! 85 | custom-protocol = ["tauri/custom-protocol"] 86 | -------------------------------------------------------------------------------- /src-tauri/bin/extract-icon-x86_64-pc-windows-msvc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/bin/extract-icon-x86_64-pc-windows-msvc.exe -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/migrated.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "migrated", 3 | "description": "permissions that were migrated from v1", 4 | "local": true, 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:path:default", 10 | "core:event:default", 11 | "core:window:default", 12 | "core:app:default", 13 | "core:resources:default", 14 | "core:menu:default", 15 | "core:tray:default", 16 | "fs:allow-read-file", 17 | "fs:allow-write-file", 18 | "fs:allow-mkdir", 19 | "fs:allow-exists", 20 | { 21 | "identifier": "fs:scope", 22 | "allow": [ 23 | "**" 24 | ] 25 | }, 26 | "shell:allow-open", 27 | "dialog:allow-open", 28 | { 29 | "identifier": "http:default", 30 | "allow": [ 31 | { 32 | "url": "https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/*" 33 | }, 34 | { 35 | "url": "https://seiya-saiga.com/game/*" 36 | }, 37 | { 38 | "url": "https://raw.githubusercontent.com/ryoha000/launcherg/*" 39 | }, 40 | { 41 | "url": "https://launcherg.ryoha.moe/*" 42 | }, 43 | { 44 | "url": "http://localhost:3248/*" 45 | } 46 | ] 47 | }, 48 | "clipboard-manager:allow-read-text", 49 | "clipboard-manager:allow-read-image", 50 | "clipboard-manager:default", 51 | "fs:default", 52 | "dialog:default", 53 | "shell:default", 54 | "http:default", 55 | "shell:default" 56 | ] 57 | } -------------------------------------------------------------------------------- /src-tauri/gen/schemas/capabilities.json: -------------------------------------------------------------------------------- 1 | {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","fs:allow-read-file","fs:allow-write-file","fs:allow-mkdir","fs:allow-exists",{"identifier":"fs:scope","allow":["**"]},"shell:allow-open","dialog:allow-open",{"identifier":"http:default","allow":[{"url":"https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/*"},{"url":"https://seiya-saiga.com/game/*"},{"url":"https://raw.githubusercontent.com/ryoha000/launcherg/*"},{"url":"https://launcherg.ryoha.moe/*"},{"url":"http://localhost:3248/*"}]},"clipboard-manager:allow-read-text","clipboard-manager:allow-read-image","clipboard-manager:default","fs:default","dialog:default","shell:default","http:default","shell:default"]}} -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/notfound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoha000/launcherg/f770fdcd07f8179bbb0e0bb7ab3298d03a4b469c/src-tauri/icons/notfound.png -------------------------------------------------------------------------------- /src-tauri/src/domain/all_game_cache.rs: -------------------------------------------------------------------------------- 1 | #[derive(derive_new::new, Debug, Clone)] 2 | pub struct AllGameCacheOne { 3 | pub id: i32, 4 | pub gamename: String, 5 | } 6 | 7 | #[derive(derive_new::new, Debug, Clone)] 8 | pub struct NewAllGameCacheOne { 9 | pub id: i32, 10 | pub gamename: String, 11 | pub thumbnail_url: String, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct AllGameCacheOneWithThumbnailUrl { 16 | pub id: i32, 17 | pub gamename: String, 18 | pub thumbnail_url: String, 19 | } 20 | 21 | pub type AllGameCache = Vec; 22 | -------------------------------------------------------------------------------- /src-tauri/src/domain/collection.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use derive_new::new; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::Id; 6 | 7 | #[derive(new, Debug)] 8 | pub struct NewCollection { 9 | pub name: String, 10 | } 11 | #[derive(new, Debug, Clone, Serialize, Deserialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct CollectionElement { 14 | pub id: Id, 15 | pub gamename: String, 16 | pub gamename_ruby: String, 17 | pub brandname: String, 18 | pub brandname_ruby: String, 19 | pub sellday: String, 20 | pub is_nukige: bool, 21 | pub exe_path: Option, 22 | pub lnk_path: Option, 23 | pub install_at: Option>, 24 | pub last_play_at: Option>, 25 | pub like_at: Option>, 26 | pub thumbnail_width: Option, 27 | pub thumbnail_height: Option, 28 | pub created_at: DateTime, 29 | pub updated_at: DateTime, 30 | } 31 | 32 | #[derive(new, Debug)] 33 | pub struct NewCollectionElement { 34 | pub id: Id, 35 | pub gamename: String, 36 | pub exe_path: Option, 37 | pub lnk_path: Option, 38 | pub install_at: Option>, 39 | } 40 | 41 | #[derive(new, Debug, Clone, Serialize, Deserialize)] 42 | pub struct NewCollectionElementDetail { 43 | pub collection_element_id: Id, 44 | pub gamename_ruby: String, 45 | pub brandname: String, 46 | pub brandname_ruby: String, 47 | pub sellday: String, 48 | pub is_nukige: bool, 49 | } 50 | -------------------------------------------------------------------------------- /src-tauri/src/domain/distance.rs: -------------------------------------------------------------------------------- 1 | pub struct Distance { 2 | a: Vec, 3 | b: Vec, 4 | m: usize, 5 | n: usize, // m <= n, m is a length, n is b length 6 | } 7 | 8 | impl Distance { 9 | pub fn new(a: &str, b: &str) -> Self { 10 | let a: Vec = a.chars().collect(); 11 | let b: Vec = b.chars().collect(); 12 | let (m, n) = (a.len(), b.len()); 13 | 14 | if m > n { 15 | return Distance { 16 | a: b, 17 | b: a, 18 | m: n, 19 | n: m, 20 | }; 21 | } 22 | 23 | Distance { a, b, m, n } 24 | } 25 | 26 | pub fn onp(&self) -> usize { 27 | let offset: isize = (self.m as isize) + 1; 28 | let delta: isize = (self.n as isize) - (self.m as isize); 29 | let mut fp = vec![-1; self.m + self.n + 3]; 30 | 31 | let mut p: isize = 0; 32 | loop { 33 | // -p <= k <= delta - 1 34 | for k in (-p)..=(delta - 1) { 35 | fp[(k + offset) as usize] = self.snake( 36 | k, 37 | (fp[(k - 1 + offset) as usize] + 1).max(fp[(k + 1 + offset) as usize]), 38 | ); 39 | } 40 | // delta + 1 <= k <= delta + p 41 | for k in ((delta + 1)..=(delta + p)).rev() { 42 | fp[(k + offset) as usize] = self.snake( 43 | k, 44 | (fp[(k - 1 + offset) as usize] + 1).max(fp[(k + 1 + offset) as usize]), 45 | ); 46 | } 47 | // delta == k 48 | fp[(delta + offset) as usize] = self.snake( 49 | delta, 50 | (fp[(delta - 1 + offset) as usize] + 1).max(fp[(delta + 1 + offset) as usize]), 51 | ); 52 | if fp[(delta + offset) as usize] == (self.n as isize) { 53 | return (delta + 2 * p) as usize; 54 | } 55 | p += 1; 56 | } 57 | } 58 | 59 | fn snake(&self, k: isize, y: isize) -> isize { 60 | let mut x = y - k; 61 | let mut y = y; 62 | while x < self.m as isize && y < self.n as isize && self.a[x as usize] == self.b[y as usize] 63 | { 64 | x += 1; 65 | y += 1; 66 | } 67 | y 68 | } 69 | } 70 | 71 | pub fn get_comparable_distance(a: &str, b: &str) -> f32 { 72 | let distance = Distance::new(&a, &b); 73 | let distance_value = distance.onp(); 74 | 75 | 1.0 - (distance_value as f32 / a.len().max(b.len()) as f32) 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | 82 | #[test] 83 | fn test_onp() { 84 | let cases = vec![("abc", "abcdef", 3), ("abc", "ab", 1), ("abc", "abc", 0)]; 85 | 86 | for (a, b, expected) in cases { 87 | let distance = Distance::new(a, b); 88 | let result = distance.onp(); 89 | assert_eq!(result, expected, "Failed on values a:{}, b:{}", a, b); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src-tauri/src/domain/explored_cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | pub type ExploredCache = HashSet; 4 | -------------------------------------------------------------------------------- /src-tauri/src/domain/explorer/file.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use async_trait::async_trait; 4 | use tauri::AppHandle; 5 | 6 | #[async_trait] 7 | pub trait FileExplorer { 8 | fn save_base64_image(&self, path: &str, data: String) -> anyhow::Result<()>; 9 | fn get_save_image_path(&self, handle: &Arc, id: i32) -> anyhow::Result; 10 | fn get_save_screenshot_path_by_name( 11 | &self, 12 | handle: &Arc, 13 | name: &str, 14 | ) -> anyhow::Result; 15 | fn get_md_path(&self, handle: &Arc, id: i32) -> anyhow::Result; 16 | } 17 | -------------------------------------------------------------------------------- /src-tauri/src/domain/explorer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod file; 2 | pub mod network; 3 | -------------------------------------------------------------------------------- /src-tauri/src/domain/explorer/network.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use crate::domain::network::ErogamescapeIDNamePair; 4 | 5 | #[async_trait] 6 | pub trait NetworkExplorer { 7 | async fn get_all_games(&self) -> anyhow::Result>; 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/src/domain/mod.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use derive_new::new; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub mod all_game_cache; 7 | pub mod collection; 8 | pub mod distance; 9 | pub mod explored_cache; 10 | pub mod file; 11 | pub mod network; 12 | pub mod process; 13 | 14 | pub mod explorer; 15 | pub mod repository; 16 | pub mod windows; 17 | 18 | #[derive(new, Debug, Clone, Copy, Serialize, Deserialize)] 19 | pub struct Id { 20 | pub value: i32, 21 | _marker: PhantomData, 22 | } 23 | -------------------------------------------------------------------------------- /src-tauri/src/domain/network.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub struct NetWork {} 4 | 5 | #[derive(Debug, Serialize, Deserialize, Clone)] 6 | pub struct ErogamescapeIDNamePair { 7 | pub id: i32, 8 | pub gamename: String, 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/src/domain/process.rs: -------------------------------------------------------------------------------- 1 | pub struct Process {} 2 | -------------------------------------------------------------------------------- /src-tauri/src/domain/repository/all_game_cache.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use chrono::{DateTime, Local}; 3 | 4 | use crate::domain::all_game_cache::{ 5 | AllGameCache, AllGameCacheOneWithThumbnailUrl, NewAllGameCacheOne, 6 | }; 7 | 8 | #[async_trait] 9 | pub trait AllGameCacheRepository { 10 | async fn get_by_ids( 11 | &self, 12 | ids: Vec, 13 | ) -> anyhow::Result>; 14 | async fn get_all(&self) -> anyhow::Result; 15 | async fn get_last_updated(&self) -> anyhow::Result<(i32, DateTime)>; 16 | async fn update(&self, cache: Vec) -> anyhow::Result<()>; 17 | async fn delete_by_ids(&self, ids: Vec) -> anyhow::Result<()>; 18 | } 19 | -------------------------------------------------------------------------------- /src-tauri/src/domain/repository/collection.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::{ 2 | collection::{CollectionElement, NewCollectionElement, NewCollectionElementDetail}, 3 | Id, 4 | }; 5 | use anyhow::Result; 6 | use async_trait::async_trait; 7 | use chrono::{DateTime, Local}; 8 | 9 | #[async_trait] 10 | pub trait CollectionRepository { 11 | async fn get_all_elements(&self) -> Result>; 12 | async fn get_element_by_element_id( 13 | &self, 14 | id: &Id, 15 | ) -> Result>; 16 | async fn upsert_collection_element(&self, new_elements: &NewCollectionElement) -> Result<()>; 17 | async fn upsert_collection_element_thumbnail_size( 18 | &self, 19 | id: &Id, 20 | width: i32, 21 | height: i32, 22 | ) -> Result<()>; 23 | async fn get_null_thumbnail_size_element_ids(&self) -> Result>>; 24 | async fn remove_conflict_maps(&self) -> Result<()>; 25 | async fn delete_collection_element(&self, element_id: &Id) -> Result<()>; 26 | 27 | async fn get_not_registered_detail_element_ids(&self) -> Result>>; 28 | async fn create_element_details(&self, details: Vec) -> Result<()>; 29 | async fn get_brandname_and_rubies(&self) -> Result>; 30 | 31 | async fn get_element_ids_by_is_nukige( 32 | &self, 33 | is_nukige: bool, 34 | ) -> Result>>; 35 | async fn get_element_ids_by_install_at_not_null(&self) -> Result>>; 36 | async fn get_element_ids_by_brandnames( 37 | &self, 38 | brandnames: &Vec, 39 | ) -> Result>>; 40 | async fn get_element_ids_by_sellday( 41 | &self, 42 | since: &str, 43 | until: &str, 44 | ) -> Result>>; 45 | 46 | async fn update_element_last_play_at_by_id( 47 | &self, 48 | id: &Id, 49 | last_play_at: DateTime, 50 | ) -> Result<()>; 51 | async fn update_element_like_at_by_id( 52 | &self, 53 | id: &Id, 54 | like_at: Option>, 55 | ) -> Result<()>; 56 | 57 | async fn delete_element_by_id(&self, id: &Id) -> Result<()>; 58 | } 59 | -------------------------------------------------------------------------------- /src-tauri/src/domain/repository/explored_cache.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use crate::domain::explored_cache::ExploredCache; 4 | 5 | #[async_trait] 6 | pub trait ExploredCacheRepository { 7 | async fn get_all(&self) -> anyhow::Result; 8 | async fn add(&self, cache: ExploredCache) -> anyhow::Result<()>; 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/src/domain/repository/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod all_game_cache; 2 | pub mod collection; 3 | pub mod explored_cache; 4 | -------------------------------------------------------------------------------- /src-tauri/src/domain/windows/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod process; 2 | -------------------------------------------------------------------------------- /src-tauri/src/domain/windows/process.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | #[async_trait] 4 | pub trait ProcessWindows { 5 | fn save_screenshot_by_process_id(&self, process_id: u32, filepath: &str) -> anyhow::Result<()>; 6 | fn save_top_window_screenshot(&self, filepath: &str) -> anyhow::Result<()>; 7 | fn get_top_window_name(&self) -> anyhow::Result; 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/explorerimpl/explorer.rs: -------------------------------------------------------------------------------- 1 | use derive_new::new; 2 | use std::marker::PhantomData; 3 | 4 | use crate::domain::{ 5 | explorer::{file::FileExplorer, network::NetworkExplorer}, 6 | file::File, 7 | network::NetWork, 8 | }; 9 | 10 | #[derive(new)] 11 | pub struct ExplorerImpl { 12 | _marker: PhantomData, 13 | } 14 | 15 | pub struct Explorers { 16 | file_explorer: ExplorerImpl, 17 | network_explorer: ExplorerImpl, 18 | } 19 | pub trait ExplorersExt { 20 | type FileExplorer: FileExplorer; 21 | type NetworkExplorer: NetworkExplorer; 22 | 23 | fn file_explorer(&self) -> &Self::FileExplorer; 24 | fn network_explorer(&self) -> &Self::NetworkExplorer; 25 | } 26 | 27 | impl ExplorersExt for Explorers { 28 | type FileExplorer = ExplorerImpl; 29 | type NetworkExplorer = ExplorerImpl; 30 | 31 | fn file_explorer(&self) -> &Self::FileExplorer { 32 | &self.file_explorer 33 | } 34 | fn network_explorer(&self) -> &Self::NetworkExplorer { 35 | &self.network_explorer 36 | } 37 | } 38 | 39 | impl Explorers { 40 | pub fn new() -> Self { 41 | let file_explorer = ExplorerImpl::new(); 42 | let network_explorer = ExplorerImpl::new(); 43 | 44 | Self { 45 | file_explorer, 46 | network_explorer, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/explorerimpl/file.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::sync::Arc; 3 | use std::{fs, path::Path}; 4 | 5 | use async_trait::async_trait; 6 | use base64::{engine::general_purpose, Engine as _}; 7 | use tauri::AppHandle; 8 | use uuid::Uuid; 9 | 10 | use crate::{ 11 | domain::{explorer::file::FileExplorer, file::File}, 12 | infrastructure::util::get_save_root_abs_dir, 13 | }; 14 | 15 | use super::explorer::ExplorerImpl; 16 | 17 | const MEMOS_ROOT_DIR: &str = "game-memos"; 18 | const SCREENSHOTS_ROOT_DIR: &str = "screenshots"; 19 | 20 | #[async_trait] 21 | impl FileExplorer for ExplorerImpl { 22 | fn save_base64_image(&self, path: &str, data: String) -> anyhow::Result<()> { 23 | let decoded_data = general_purpose::STANDARD_NO_PAD.decode(data)?; 24 | 25 | let mut file = std::fs::File::create(path)?; 26 | file.write_all(&decoded_data)?; 27 | Ok(()) 28 | } 29 | fn get_save_image_path(&self, handle: &Arc, id: i32) -> anyhow::Result { 30 | let dir = Path::new(&get_save_root_abs_dir(handle)) 31 | .join(MEMOS_ROOT_DIR) 32 | .join(id.to_string()); 33 | fs::create_dir_all(&dir).unwrap(); 34 | Ok(Path::new(&dir) 35 | .join(format!("{}.png", Uuid::new_v4().to_string())) 36 | .to_string_lossy() 37 | .to_string()) 38 | } 39 | fn get_save_screenshot_path_by_name( 40 | &self, 41 | handle: &Arc, 42 | name: &str, 43 | ) -> anyhow::Result { 44 | let dir = Path::new(&get_save_root_abs_dir(handle)).join(SCREENSHOTS_ROOT_DIR); 45 | fs::create_dir_all(&dir).unwrap(); 46 | let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); 47 | Ok(Path::new(&dir) 48 | .join(format!("{}-{}.png", name, timestamp)) 49 | .to_string_lossy() 50 | .to_string()) 51 | } 52 | fn get_md_path(&self, handle: &Arc, id: i32) -> anyhow::Result { 53 | let dir = Path::new(&get_save_root_abs_dir(handle)) 54 | .join(MEMOS_ROOT_DIR) 55 | .join(id.to_string()); 56 | fs::create_dir_all(&dir).unwrap(); 57 | Ok(Path::new(&dir) 58 | .join("untitled.md") 59 | .to_string_lossy() 60 | .to_string()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/explorerimpl/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod explorer; 2 | pub mod file; 3 | pub mod network; 4 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/explorerimpl/network.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use super::explorer::ExplorerImpl; 4 | use crate::domain::{ 5 | explorer::network::NetworkExplorer, 6 | network::{ErogamescapeIDNamePair, NetWork}, 7 | }; 8 | 9 | #[async_trait] 10 | impl NetworkExplorer for ExplorerImpl { 11 | async fn get_all_games(&self) -> anyhow::Result> { 12 | Ok(reqwest::get( 13 | "https://raw.githubusercontent.com/ryoha000/launcherg/main/script/all_games.json", 14 | ) 15 | .await? 16 | .json::>() 17 | .await?) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod explorerimpl; 2 | pub mod repositoryimpl; 3 | pub mod util; 4 | pub mod windowsimpl; 5 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/all_game_cahe.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use chrono::{DateTime, Local, NaiveDateTime}; 3 | use sqlx::{query_as, QueryBuilder, Row}; 4 | 5 | use crate::domain::{ 6 | all_game_cache::{AllGameCache, AllGameCacheOneWithThumbnailUrl, NewAllGameCacheOne}, 7 | repository::all_game_cache::AllGameCacheRepository, 8 | }; 9 | 10 | use super::{models::all_game_cache::AllGameCacheTable, repository::RepositoryImpl}; 11 | 12 | #[async_trait] 13 | impl AllGameCacheRepository for RepositoryImpl { 14 | async fn get_by_ids( 15 | &self, 16 | ids: Vec, 17 | ) -> anyhow::Result> { 18 | let pool = self.pool.0.clone(); 19 | let mut builder = 20 | sqlx::query_builder::QueryBuilder::new("SELECT * from all_game_caches WHERE id IN ("); 21 | let mut separated = builder.separated(", "); 22 | for id in ids.iter() { 23 | separated.push_bind(id); 24 | } 25 | separated.push_unseparated(")"); 26 | let query = builder.build(); 27 | Ok(query 28 | .fetch_all(&*pool) 29 | .await? 30 | .into_iter() 31 | .map(|v| AllGameCacheOneWithThumbnailUrl { 32 | id: v.get(0), 33 | gamename: v.get(1), 34 | thumbnail_url: v.get(2), 35 | }) 36 | .collect()) 37 | } 38 | async fn get_all(&self) -> anyhow::Result { 39 | let pool = self.pool.0.clone(); 40 | Ok( 41 | query_as::<_, AllGameCacheTable>("select * from all_game_caches") 42 | .fetch_all(&*pool) 43 | .await? 44 | .into_iter() 45 | .filter_map(|v| v.try_into().ok()) 46 | .collect(), 47 | ) 48 | } 49 | async fn get_last_updated(&self) -> anyhow::Result<(i32, DateTime)> { 50 | let pool = self.pool.0.clone(); 51 | let last_updated: (i32, NaiveDateTime) = 52 | sqlx::query_as("SELECT MAX(id), MAX(created_at) from all_game_caches") 53 | .fetch_one(&*pool) 54 | .await?; 55 | Ok(( 56 | last_updated.0, 57 | last_updated.1.and_utc().with_timezone(&Local), 58 | )) 59 | } 60 | async fn update(&self, cache: Vec) -> anyhow::Result<()> { 61 | if cache.len() == 0 { 62 | return Ok(()); 63 | } 64 | for c in cache.chunks(1000) { 65 | // ref: https://docs.rs/sqlx-core/latest/sqlx_core/query_builder/struct.QueryBuilder.html#method.push_values 66 | let mut query_builder = 67 | QueryBuilder::new("INSERT INTO all_game_caches (id, gamename, thumbnail_url) "); 68 | query_builder.push_values(c, |mut b, new| { 69 | b.push_bind(new.id); 70 | b.push_bind(new.gamename.clone()); 71 | b.push_bind(new.thumbnail_url.clone()); 72 | }); 73 | 74 | let pool = self.pool.0.clone(); 75 | let query = query_builder.build(); 76 | query.execute(&*pool).await?; 77 | } 78 | Ok(()) 79 | } 80 | async fn delete_by_ids(&self, ids: Vec) -> anyhow::Result<()> { 81 | if ids.len() == 0 { 82 | return Ok(()); 83 | } 84 | let pool = self.pool.0.clone(); 85 | let mut builder = QueryBuilder::new("DELETE FROM all_game_caches WHERE id IN ("); 86 | let mut separated = builder.separated(", "); 87 | for id in ids.iter() { 88 | separated.push_bind(id); 89 | } 90 | separated.push_unseparated(")"); 91 | let query = builder.build(); 92 | query.execute(&*pool).await?; 93 | Ok(()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/driver.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, str::FromStr, sync::Arc}; 2 | 3 | use refinery::config::{Config, ConfigDbType}; 4 | use sqlx::{ 5 | sqlite::{SqliteConnectOptions, SqlitePoolOptions}, 6 | Pool, Sqlite, 7 | }; 8 | use tauri::AppHandle; 9 | 10 | use crate::infrastructure::util::get_save_root_abs_dir_with_ptr_handle; 11 | 12 | #[derive(Clone)] 13 | pub struct Db(pub(crate) Arc>); 14 | 15 | const DB_FILE: &str = "launcherg_sqlite.db3"; 16 | 17 | mod embedded { 18 | use refinery::embed_migrations; 19 | embed_migrations!("./src/migrations"); 20 | } 21 | 22 | impl Db { 23 | pub async fn new(handle: &AppHandle) -> Db { 24 | let root = get_save_root_abs_dir_with_ptr_handle(handle); 25 | let db_filename = Path::new(&root) 26 | .join(Path::new(DB_FILE)) 27 | .as_path() 28 | .to_str() 29 | .unwrap() 30 | .to_string(); 31 | let pool = SqlitePoolOptions::new() 32 | .max_connections(256) 33 | .connect_with( 34 | SqliteConnectOptions::from_str(&format!("sqlite://{}?mode=rwc", db_filename)) 35 | .unwrap() 36 | .foreign_keys(true), 37 | ) 38 | .await 39 | .map_err(|err| format!("{}\nfile: {}", err.to_string(), db_filename)) 40 | .unwrap(); 41 | 42 | // migrate 43 | let mut conf = Config::new(ConfigDbType::Sqlite).set_db_path(&db_filename); 44 | embedded::migrations::runner() 45 | .set_abort_divergent(false) 46 | .run(&mut conf) 47 | .unwrap(); 48 | 49 | println!("finish setup database. file: {:?}", db_filename); 50 | 51 | Db(Arc::new(pool)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/explored_cache.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use sqlx::QueryBuilder; 3 | 4 | use crate::domain::{ 5 | explored_cache::ExploredCache, repository::explored_cache::ExploredCacheRepository, 6 | }; 7 | 8 | use super::repository::RepositoryImpl; 9 | 10 | #[async_trait] 11 | impl ExploredCacheRepository for RepositoryImpl { 12 | async fn get_all(&self) -> anyhow::Result { 13 | let pool = self.pool.0.clone(); 14 | let paths: Vec<(String,)> = sqlx::query_as("SELECT path from explored_caches") 15 | .fetch_all(&*pool) 16 | .await?; 17 | Ok(paths.into_iter().map(|v| v.0).collect()) 18 | } 19 | async fn add(&self, cache: ExploredCache) -> anyhow::Result<()> { 20 | if cache.len() == 0 { 21 | return Ok(()); 22 | } 23 | // ref: https://docs.rs/sqlx-core/latest/sqlx_core/query_builder/struct.QueryBuilder.html#method.push_values 24 | let mut query_builder = QueryBuilder::new("INSERT INTO explored_caches (path) "); 25 | query_builder.push_values(cache, |mut b, new| { 26 | b.push_bind(new); 27 | }); 28 | 29 | let pool = self.pool.0.clone(); 30 | let query = query_builder.build(); 31 | query.execute(&*pool).await?; 32 | Ok(()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod all_game_cahe; 2 | pub mod collection; 3 | pub mod driver; 4 | pub mod explored_cache; 5 | pub mod models; 6 | pub mod repository; 7 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/models/all_game_cache.rs: -------------------------------------------------------------------------------- 1 | use sqlx::types::chrono::NaiveDateTime; 2 | use sqlx::FromRow; 3 | 4 | use crate::domain::all_game_cache::AllGameCacheOne; 5 | 6 | #[derive(FromRow)] 7 | pub struct AllGameCacheTable { 8 | pub id: i32, 9 | pub gamename: String, 10 | pub thumbnail_url: String, 11 | pub created_at: NaiveDateTime, 12 | } 13 | 14 | impl TryFrom for AllGameCacheOne { 15 | type Error = anyhow::Error; 16 | fn try_from(st: AllGameCacheTable) -> Result { 17 | Ok(AllGameCacheOne { 18 | id: st.id, 19 | gamename: st.gamename, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/models/collection.rs: -------------------------------------------------------------------------------- 1 | use chrono::Local; 2 | use sqlx::types::chrono::NaiveDateTime; 3 | use sqlx::FromRow; 4 | 5 | use crate::domain::{collection::CollectionElement, Id}; 6 | 7 | #[derive(FromRow)] 8 | pub struct CollectionElementTable { 9 | pub id: i32, 10 | pub gamename: String, 11 | pub gamename_ruby: String, 12 | pub brandname: String, 13 | pub brandname_ruby: String, 14 | pub sellday: String, 15 | pub is_nukige: i32, 16 | pub exe_path: Option, 17 | pub lnk_path: Option, 18 | pub install_at: Option, 19 | pub last_play_at: Option, 20 | pub like_at: Option, 21 | pub thumbnail_width: Option, 22 | pub thumbnail_height: Option, 23 | pub created_at: NaiveDateTime, 24 | pub updated_at: NaiveDateTime, 25 | } 26 | 27 | impl TryFrom for CollectionElement { 28 | type Error = anyhow::Error; 29 | fn try_from(st: CollectionElementTable) -> Result { 30 | Ok(CollectionElement::new( 31 | Id::new(st.id), 32 | st.gamename, 33 | st.gamename_ruby, 34 | st.brandname, 35 | st.brandname_ruby, 36 | st.sellday, 37 | st.is_nukige != 0, 38 | st.exe_path, 39 | st.lnk_path, 40 | st.install_at 41 | .and_then(|v| Some(v.and_utc().with_timezone(&Local))), 42 | st.last_play_at 43 | .and_then(|v| Some(v.and_utc().with_timezone(&Local))), 44 | st.like_at 45 | .and_then(|v| Some(v.and_utc().with_timezone(&Local))), 46 | st.thumbnail_width, 47 | st.thumbnail_height, 48 | st.created_at.and_utc().with_timezone(&Local), 49 | st.updated_at.and_utc().with_timezone(&Local), 50 | )) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod all_game_cache; 2 | pub mod collection; 3 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/repositoryimpl/repository.rs: -------------------------------------------------------------------------------- 1 | use derive_new::new; 2 | use std::marker::PhantomData; 3 | 4 | use crate::domain::{ 5 | all_game_cache::AllGameCache, 6 | collection::CollectionElement, 7 | explored_cache::ExploredCache, 8 | repository::{ 9 | all_game_cache::AllGameCacheRepository, collection::CollectionRepository, 10 | explored_cache::ExploredCacheRepository, 11 | }, 12 | }; 13 | 14 | use super::driver::Db; 15 | 16 | #[derive(new)] 17 | pub struct RepositoryImpl { 18 | pub pool: Db, 19 | _marker: PhantomData, 20 | } 21 | 22 | pub struct Repositories { 23 | collection_repository: RepositoryImpl, 24 | explored_cache_repository: RepositoryImpl, 25 | all_game_cache_repository: RepositoryImpl, 26 | } 27 | pub trait RepositoriesExt { 28 | type CollectionRepo: CollectionRepository; 29 | type ExploredCacheRepo: ExploredCacheRepository; 30 | type AllGameCacheRepo: AllGameCacheRepository; 31 | 32 | fn collection_repository(&self) -> &Self::CollectionRepo; 33 | fn explored_cache_repository(&self) -> &Self::ExploredCacheRepo; 34 | fn all_game_cache_repository(&self) -> &Self::AllGameCacheRepo; 35 | } 36 | 37 | impl RepositoriesExt for Repositories { 38 | type CollectionRepo = RepositoryImpl; 39 | type ExploredCacheRepo = RepositoryImpl; 40 | type AllGameCacheRepo = RepositoryImpl; 41 | 42 | fn collection_repository(&self) -> &Self::CollectionRepo { 43 | &self.collection_repository 44 | } 45 | fn explored_cache_repository(&self) -> &Self::ExploredCacheRepo { 46 | &self.explored_cache_repository 47 | } 48 | fn all_game_cache_repository(&self) -> &Self::AllGameCacheRepo { 49 | &self.all_game_cache_repository 50 | } 51 | } 52 | 53 | impl Repositories { 54 | pub fn new(db: Db) -> Self { 55 | let collection_repository = RepositoryImpl::new(db.clone()); 56 | let explored_cache_repository = RepositoryImpl::new(db.clone()); 57 | let all_game_cache_repository = RepositoryImpl::new(db.clone()); 58 | 59 | Self { 60 | collection_repository, 61 | explored_cache_repository, 62 | all_game_cache_repository, 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/util.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | use std::{fs, path::Path}; 4 | 5 | use tauri::AppHandle; 6 | use tauri::Manager; 7 | 8 | const ROOT_DIR: &str = "launcherg"; 9 | 10 | fn get_abs_dir(root: Option) -> String { 11 | match root { 12 | Some(root) => { 13 | let path = &root.join(Path::new(ROOT_DIR)); 14 | fs::create_dir_all(path).unwrap(); 15 | return path.to_string_lossy().to_string(); 16 | } 17 | None => { 18 | fs::create_dir_all(ROOT_DIR).unwrap(); 19 | return fs::canonicalize(ROOT_DIR) 20 | .unwrap() 21 | .to_string_lossy() 22 | .to_string(); 23 | } 24 | } 25 | } 26 | 27 | pub fn get_save_root_abs_dir(handle: &Arc) -> String { 28 | let root = handle.path().app_config_dir().ok(); 29 | get_abs_dir(root) 30 | } 31 | 32 | pub fn get_save_root_abs_dir_with_ptr_handle(handle: &AppHandle) -> String { 33 | let root = handle.path().app_config_dir().ok(); 34 | get_abs_dir(root) 35 | } 36 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/windowsimpl/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod process; 2 | mod screenshot; 3 | pub mod windows; 4 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/windowsimpl/process.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use windows::Win32::{ 3 | Foundation::MAX_PATH, 4 | UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowTextW}, 5 | }; 6 | 7 | use super::{screenshot::take, windows::WindowsImpl}; 8 | use crate::domain::{process::Process, windows::process::ProcessWindows}; 9 | 10 | #[async_trait] 11 | impl ProcessWindows for WindowsImpl { 12 | fn save_screenshot_by_process_id(&self, process_id: u32, filepath: &str) -> anyhow::Result<()> { 13 | take::take_screenshot_by_process_id(process_id, filepath) 14 | } 15 | fn save_top_window_screenshot(&self, filepath: &str) -> anyhow::Result<()> { 16 | take::take_screenshot_by_top_window(filepath) 17 | } 18 | fn get_top_window_name(&self) -> anyhow::Result { 19 | let hwnd = unsafe { GetForegroundWindow() }; 20 | if hwnd.0 == 0 { 21 | return Err(anyhow::anyhow!("cannot get top window")); 22 | } 23 | let mut window_text = vec![0u16; MAX_PATH as usize]; 24 | unsafe { GetWindowTextW(hwnd, &mut window_text.as_mut_slice()) }; 25 | Ok(String::from_utf16_lossy(&window_text) 26 | .trim_end_matches('\0') 27 | .to_string()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/windowsimpl/screenshot/README.md: -------------------------------------------------------------------------------- 1 | This directory is cloned from https://github.com/robmikh/screenshot-rs\ 2 | Thanks 3 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/windowsimpl/screenshot/capture.rs: -------------------------------------------------------------------------------- 1 | use sysinfo::{PidExt, ProcessExt, SystemExt}; 2 | use windows::Win32::{ 3 | Foundation::{BOOL, HWND, LPARAM, RECT, TRUE}, 4 | UI::WindowsAndMessaging::{GetWindowInfo, GetWindowThreadProcessId, WINDOWINFO}, 5 | }; 6 | 7 | struct FindWindowData { 8 | process_id: u32, 9 | hwnds: Vec, 10 | } 11 | 12 | unsafe extern "system" fn enum_window_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { 13 | let data = &mut *(lparam.0 as *mut FindWindowData); 14 | let process_id = &mut 0; 15 | GetWindowThreadProcessId(hwnd, Some(process_id)); 16 | if *process_id == data.process_id { 17 | data.hwnds.push(hwnd); 18 | } 19 | TRUE 20 | } 21 | 22 | fn find_window_handles_by_process_id(process_id: u32) -> anyhow::Result> { 23 | let mut data: FindWindowData = FindWindowData { 24 | process_id, 25 | hwnds: vec![], 26 | }; 27 | 28 | unsafe { 29 | windows::Win32::UI::WindowsAndMessaging::EnumWindows( 30 | Some(enum_window_proc), 31 | LPARAM(&mut data as *mut _ as isize), 32 | )?; 33 | } 34 | Ok(data.hwnds) 35 | } 36 | 37 | struct ProcessIdCandidate { 38 | started: Option, 39 | children: Vec, 40 | } 41 | fn find_candidate_process_ids_by_started(started_process_id: u32) -> ProcessIdCandidate { 42 | let mut system = sysinfo::System::new_all(); 43 | system.refresh_all(); 44 | 45 | let mut process_id_candidates: ProcessIdCandidate = ProcessIdCandidate { 46 | started: None, 47 | children: vec![], 48 | }; 49 | 50 | for (process_id, process) in system.processes() { 51 | if process_id.as_u32() == started_process_id { 52 | process_id_candidates.started = Some(started_process_id); 53 | } else if process.parent().map(|p| p.as_u32()).unwrap_or(0) == started_process_id { 54 | process_id_candidates.children.push(process_id.as_u32()); 55 | } 56 | } 57 | process_id_candidates 58 | } 59 | 60 | fn get_window_info(hwnd: HWND) -> anyhow::Result { 61 | let mut wi = WINDOWINFO::default(); 62 | wi.cbSize = std::mem::size_of::() as u32; 63 | unsafe { GetWindowInfo(hwnd, &mut wi)? }; 64 | 65 | Ok(wi.rcClient) 66 | } 67 | 68 | fn find_capturable_window_by_pid(pid: u32) -> anyhow::Result { 69 | find_window_handles_by_process_id(pid).and_then(|hwnds| { 70 | hwnds 71 | .into_iter() 72 | .find(|hwnd| { 73 | get_window_info(*hwnd) 74 | .map(|rect| { 75 | let width = rect.right - rect.left; 76 | let height = rect.bottom - rect.top; 77 | println!("process_id: {}, width: {}, height: {}", pid, width, height); 78 | if width > 400 && height > 200 { 79 | true 80 | } else { 81 | false 82 | } 83 | }) 84 | .unwrap_or(false) 85 | }) 86 | .ok_or_else(|| anyhow::anyhow!("No capture window found")) 87 | }) 88 | } 89 | 90 | pub fn find_capture_hwnd(started_process_id: u32) -> anyhow::Result { 91 | let process_id_candidates = find_candidate_process_ids_by_started(started_process_id); 92 | if let Some(process_id) = process_id_candidates.started { 93 | if let Ok(hwnd) = find_capturable_window_by_pid(process_id) { 94 | println!("Found capture window by started pid: {}", process_id); 95 | return Ok(hwnd); 96 | } 97 | } 98 | for process_id in process_id_candidates.children { 99 | if let Ok(hwnd) = find_capturable_window_by_pid(process_id) { 100 | println!("Found capture window by pid: {}", process_id); 101 | return Ok(hwnd); 102 | } 103 | } 104 | Err(anyhow::anyhow!("No capture window found")) 105 | } 106 | 107 | pub fn find_top_window() -> anyhow::Result { 108 | let hwnd = unsafe { windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow() }; 109 | if hwnd.0 == 0 as isize { 110 | return Err(anyhow::anyhow!("No top window found")); 111 | } 112 | Ok(hwnd) 113 | } 114 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/windowsimpl/screenshot/d3d.rs: -------------------------------------------------------------------------------- 1 | use windows::core::{ComInterface, Interface, Result}; 2 | use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice; 3 | use windows::Win32::Graphics::{ 4 | Direct3D::{D3D_DRIVER_TYPE, D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP}, 5 | Direct3D11::{ 6 | D3D11CreateDevice, ID3D11Device, D3D11_CREATE_DEVICE_BGRA_SUPPORT, 7 | D3D11_CREATE_DEVICE_FLAG, D3D11_SDK_VERSION, 8 | }, 9 | Dxgi::{IDXGIDevice, DXGI_ERROR_UNSUPPORTED}, 10 | }; 11 | use windows::Win32::System::WinRT::Direct3D11::{ 12 | CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess, 13 | }; 14 | 15 | fn create_d3d_device_with_type( 16 | driver_type: D3D_DRIVER_TYPE, 17 | flags: D3D11_CREATE_DEVICE_FLAG, 18 | device: *mut Option, 19 | ) -> Result<()> { 20 | unsafe { 21 | D3D11CreateDevice( 22 | None, 23 | driver_type, 24 | None, 25 | flags, 26 | None, 27 | D3D11_SDK_VERSION, 28 | Some(device), 29 | None, 30 | None, 31 | ) 32 | } 33 | } 34 | 35 | pub fn create_d3d_device() -> Result { 36 | let mut device = None; 37 | let mut result = create_d3d_device_with_type( 38 | D3D_DRIVER_TYPE_HARDWARE, 39 | D3D11_CREATE_DEVICE_BGRA_SUPPORT, 40 | &mut device, 41 | ); 42 | if let Err(error) = &result { 43 | if error.code() == DXGI_ERROR_UNSUPPORTED { 44 | result = create_d3d_device_with_type( 45 | D3D_DRIVER_TYPE_WARP, 46 | D3D11_CREATE_DEVICE_BGRA_SUPPORT, 47 | &mut device, 48 | ); 49 | } 50 | } 51 | result?; 52 | Ok(device.unwrap()) 53 | } 54 | 55 | pub fn create_direct3d_device(d3d_device: &ID3D11Device) -> Result { 56 | let dxgi_device: IDXGIDevice = d3d_device.cast()?; 57 | let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device)? }; 58 | inspectable.cast() 59 | } 60 | 61 | pub fn get_d3d_interface_from_object( 62 | object: &S, 63 | ) -> Result { 64 | let access: IDirect3DDxgiInterfaceAccess = object.cast()?; 65 | let object = unsafe { access.GetInterface::()? }; 66 | Ok(object) 67 | } 68 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/windowsimpl/screenshot/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod capture; 2 | pub mod d3d; 3 | pub mod take; 4 | -------------------------------------------------------------------------------- /src-tauri/src/infrastructure/windowsimpl/windows.rs: -------------------------------------------------------------------------------- 1 | use derive_new::new; 2 | use std::marker::PhantomData; 3 | 4 | use crate::domain::{process::Process, windows::process::ProcessWindows}; 5 | 6 | #[derive(new)] 7 | pub struct WindowsImpl { 8 | _marker: PhantomData, 9 | } 10 | 11 | pub struct Windows { 12 | process: WindowsImpl, 13 | } 14 | pub trait WindowsExt { 15 | type ProcessWindows: ProcessWindows; 16 | 17 | fn process(&self) -> &Self::ProcessWindows; 18 | } 19 | 20 | impl WindowsExt for Windows { 21 | type ProcessWindows = WindowsImpl; 22 | 23 | fn process(&self) -> &Self::ProcessWindows { 24 | &self.process 25 | } 26 | } 27 | 28 | impl Windows { 29 | pub fn new() -> Self { 30 | let process = WindowsImpl::new(); 31 | 32 | Self { process } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src-tauri/src/interface/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum CommandError { 3 | #[error(transparent)] 4 | Anyhow(#[from] anyhow::Error), 5 | } 6 | 7 | impl serde::Serialize for CommandError { 8 | fn serialize(&self, serializer: S) -> Result 9 | where 10 | S: serde::ser::Serializer, 11 | { 12 | serializer.serialize_str(self.to_string().as_ref()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-tauri/src/interface/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | pub mod error; 3 | pub mod models; 4 | pub mod module; 5 | -------------------------------------------------------------------------------- /src-tauri/src/interface/models/all_game_cache.rs: -------------------------------------------------------------------------------- 1 | use derive_new::new; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::domain::{self, all_game_cache::AllGameCacheOneWithThumbnailUrl}; 5 | 6 | #[derive(new, Serialize, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct AllGameCacheOne { 9 | pub id: i32, 10 | pub gamename: String, 11 | pub thumbnail_url: String, 12 | } 13 | 14 | impl From for AllGameCacheOne { 15 | fn from(st: AllGameCacheOneWithThumbnailUrl) -> Self { 16 | AllGameCacheOne::new(st.id, st.gamename, st.thumbnail_url) 17 | } 18 | } 19 | 20 | impl From for domain::all_game_cache::AllGameCacheOne { 21 | fn from(st: AllGameCacheOne) -> Self { 22 | domain::all_game_cache::AllGameCacheOne::new(st.id, st.gamename) 23 | } 24 | } 25 | 26 | impl From for domain::all_game_cache::NewAllGameCacheOne { 27 | fn from(st: AllGameCacheOne) -> Self { 28 | domain::all_game_cache::NewAllGameCacheOne::new(st.id, st.gamename, st.thumbnail_url) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/src/interface/models/collection.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use derive_new::new; 4 | use serde::{Deserialize, Serialize}; 5 | use tauri::AppHandle; 6 | 7 | use crate::domain::{ 8 | self, 9 | file::{get_icon_path, get_thumbnail_path}, 10 | }; 11 | 12 | #[derive(new, Serialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct CollectionElement { 15 | pub id: i32, 16 | pub gamename: String, 17 | pub gamename_ruby: String, 18 | pub brandname: String, 19 | pub brandname_ruby: String, 20 | pub sellday: String, 21 | pub is_nukige: bool, 22 | pub exe_path: Option, 23 | pub lnk_path: Option, 24 | pub thumbnail: String, 25 | pub icon: String, 26 | pub install_at: Option, 27 | pub last_play_at: Option, 28 | pub like_at: Option, 29 | pub registered_at: String, 30 | pub thumbnail_width: Option, 31 | pub thumbnail_height: Option, 32 | } 33 | 34 | impl CollectionElement { 35 | pub fn from_domain(handle: &Arc, st: domain::collection::CollectionElement) -> Self { 36 | CollectionElement::new( 37 | st.id.value, 38 | st.gamename, 39 | st.gamename_ruby, 40 | st.brandname, 41 | st.brandname_ruby, 42 | st.sellday, 43 | st.is_nukige, 44 | st.exe_path, 45 | st.lnk_path, 46 | get_thumbnail_path(handle, &st.id), 47 | get_icon_path(handle, &st.id), 48 | st.install_at.and_then(|v| Some(v.to_rfc3339())), 49 | st.last_play_at.and_then(|v| Some(v.to_rfc3339())), 50 | st.like_at.and_then(|v| Some(v.to_rfc3339())), 51 | st.updated_at.to_rfc3339(), 52 | st.thumbnail_width, 53 | st.thumbnail_height, 54 | ) 55 | } 56 | } 57 | 58 | #[derive(Serialize, Deserialize)] 59 | pub struct CalculateDistanceKV { 60 | pub key: String, 61 | pub value: String, 62 | } 63 | 64 | // the payload type must implement `Serialize` and `Clone`. 65 | #[derive(new, Clone, serde::Serialize)] 66 | pub struct ProgressPayload { 67 | pub message: String, 68 | } 69 | 70 | #[derive(new, Clone, serde::Serialize)] 71 | pub struct ProgressLivePayload { 72 | pub max: Option, 73 | } 74 | -------------------------------------------------------------------------------- /src-tauri/src/interface/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod all_game_cache; 2 | pub mod collection; 3 | -------------------------------------------------------------------------------- /src-tauri/src/interface/module.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use tauri::AppHandle; 4 | 5 | use crate::{ 6 | infrastructure::{ 7 | explorerimpl::explorer::{Explorers, ExplorersExt}, 8 | repositoryimpl::{ 9 | driver::Db, 10 | repository::{Repositories, RepositoriesExt}, 11 | }, 12 | windowsimpl::windows::{Windows, WindowsExt}, 13 | }, 14 | usecase::{ 15 | all_game_cache::AllGameCacheUseCase, collection::CollectionUseCase, 16 | explored_cache::ExploredCacheUseCase, file::FileUseCase, network::NetworkUseCase, 17 | process::ProcessUseCase, 18 | }, 19 | }; 20 | 21 | pub struct Modules { 22 | collection_use_case: CollectionUseCase, 23 | explored_cache_use_case: ExploredCacheUseCase, 24 | network_use_case: NetworkUseCase, 25 | file_use_case: FileUseCase, 26 | all_game_cache_use_case: AllGameCacheUseCase, 27 | process_use_case: ProcessUseCase, 28 | } 29 | pub trait ModulesExt { 30 | type Repositories: RepositoriesExt; 31 | type Explorers: ExplorersExt; 32 | type Windows: WindowsExt; 33 | 34 | fn collection_use_case(&self) -> &CollectionUseCase; 35 | fn explored_cache_use_case(&self) -> &ExploredCacheUseCase; 36 | fn all_game_cache_use_case(&self) -> &AllGameCacheUseCase; 37 | fn network_use_case(&self) -> &NetworkUseCase; 38 | fn file_use_case(&self) -> &FileUseCase; 39 | fn process_use_case(&self) -> &ProcessUseCase; 40 | } 41 | 42 | impl ModulesExt for Modules { 43 | type Repositories = Repositories; 44 | type Explorers = Explorers; 45 | type Windows = Windows; 46 | 47 | fn collection_use_case(&self) -> &CollectionUseCase { 48 | &self.collection_use_case 49 | } 50 | fn explored_cache_use_case(&self) -> &ExploredCacheUseCase { 51 | &self.explored_cache_use_case 52 | } 53 | fn all_game_cache_use_case(&self) -> &AllGameCacheUseCase { 54 | &self.all_game_cache_use_case 55 | } 56 | fn network_use_case(&self) -> &NetworkUseCase { 57 | &self.network_use_case 58 | } 59 | fn file_use_case(&self) -> &FileUseCase { 60 | &self.file_use_case 61 | } 62 | fn process_use_case(&self) -> &ProcessUseCase { 63 | &self.process_use_case 64 | } 65 | } 66 | 67 | impl Modules { 68 | pub async fn new(handle: &AppHandle) -> Self { 69 | let db = Db::new(handle).await; 70 | 71 | let repositories = Arc::new(Repositories::new(db.clone())); 72 | let explorers = Arc::new(Explorers::new()); 73 | let windows = Arc::new(Windows::new()); 74 | 75 | let collection_use_case = CollectionUseCase::new(repositories.clone()); 76 | let explored_cache_use_case = ExploredCacheUseCase::new(repositories.clone()); 77 | let all_game_cache_use_case: AllGameCacheUseCase = 78 | AllGameCacheUseCase::new(repositories.clone()); 79 | 80 | let network_use_case: NetworkUseCase = NetworkUseCase::new(explorers.clone()); 81 | let file_use_case: FileUseCase = FileUseCase::new(explorers.clone()); 82 | 83 | let process_use_case: ProcessUseCase = ProcessUseCase::new(windows.clone()); 84 | 85 | Self { 86 | collection_use_case, 87 | explored_cache_use_case, 88 | all_game_cache_use_case, 89 | network_use_case, 90 | file_use_case, 91 | process_use_case, 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod domain; 5 | mod infrastructure; 6 | mod interface; 7 | mod usecase; 8 | 9 | use std::sync::Arc; 10 | 11 | use infrastructure::util::get_save_root_abs_dir_with_ptr_handle; 12 | use interface::{command, module::Modules}; 13 | use tauri::{async_runtime::block_on, Manager}; 14 | use tauri_plugin_log::{Target, TargetKind}; 15 | 16 | fn main() { 17 | tauri::Builder::default() 18 | .plugin(tauri_plugin_http::init()) 19 | .plugin(tauri_plugin_shell::init()) 20 | .plugin(tauri_plugin_dialog::init()) 21 | .plugin(tauri_plugin_fs::init()) 22 | .plugin(tauri_plugin_clipboard_manager::init()) 23 | .setup(|app| { 24 | // folder の中身を移動して folder を削除する 25 | // C:\Users\ryoha\AppData\Roaming\launcherg -> C:\Users\ryoha\AppData\Roaming\ryoha.moe\launcherg 26 | 27 | let dst_dir = get_save_root_abs_dir_with_ptr_handle(app.handle()); 28 | let src_dir = std::path::Path::new(&dst_dir) 29 | .parent() 30 | .unwrap() 31 | .parent() 32 | .unwrap() 33 | .join("launcherg"); 34 | println!("src_dir: {:?}, dst_dir: {:?}", src_dir, dst_dir); 35 | if src_dir.exists() { 36 | let dst_dir = std::path::Path::new(&dst_dir); 37 | std::fs::create_dir_all(&dst_dir).unwrap(); 38 | for entry in std::fs::read_dir(&src_dir).unwrap() { 39 | let entry = entry.unwrap(); 40 | let path = entry.path(); 41 | let file_name = path.file_name().unwrap(); 42 | let dst_path = dst_dir.join(file_name); 43 | println!("rename {:?} -> {:?}", path, dst_path); 44 | std::fs::rename(path, dst_path).unwrap(); 45 | } 46 | std::fs::remove_dir_all(src_dir).unwrap(); 47 | } 48 | 49 | let modules = Arc::new(block_on(Modules::new(app.handle()))); 50 | app.manage(modules); 51 | 52 | Ok(()) 53 | }) 54 | .plugin( 55 | tauri_plugin_log::Builder::new() 56 | .targets([ 57 | Target::new(TargetKind::Stdout), 58 | Target::new(TargetKind::LogDir { file_name: None }), 59 | Target::new(TargetKind::Webview), 60 | ]) 61 | .build(), 62 | ) 63 | .invoke_handler(tauri::generate_handler![ 64 | command::create_elements_in_pc, 65 | command::get_nearest_key_and_distance, 66 | command::upload_image, 67 | command::upsert_collection_element, 68 | command::update_collection_element_icon, 69 | command::get_default_import_dirs, 70 | command::play_game, 71 | command::get_play_time_minutes, 72 | command::get_collection_element, 73 | command::delete_collection_element, 74 | command::get_not_registered_detail_element_ids, 75 | command::create_element_details, 76 | command::get_all_elements, 77 | command::update_element_like, 78 | command::open_folder, 79 | command::get_all_game_cache_last_updated, 80 | command::update_all_game_cache, 81 | command::get_game_candidates, 82 | command::get_exe_path_by_lnk, 83 | command::get_game_cache_by_id, 84 | command::save_screenshot_by_pid, 85 | command::update_collection_element_thumbnails, 86 | ]) 87 | .run(tauri::generate_context!()) 88 | .expect("error while running tauri application"); 89 | } 90 | -------------------------------------------------------------------------------- /src-tauri/src/migrations/V1__init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS collections ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | name TEXT NOT NULL, 4 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 5 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 6 | UNIQUE(name) 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS collection_elements ( 10 | id INTEGER PRIMARY KEY, 11 | gamename TEXT NOT NULL, 12 | exe_path TEXT, 13 | lnk_path TEXT, 14 | install_at DATETIME, 15 | last_play_at DATETIME, 16 | like_at DATETIME, 17 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 18 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 19 | ); 20 | 21 | CREATE TABLE IF NOT EXISTS collection_element_maps ( 22 | id INTEGER PRIMARY KEY AUTOINCREMENT, 23 | collection_id INTEGER NOT NULL, 24 | collection_element_id INTEGER NOT NULL, 25 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 26 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 27 | foreign key(collection_id) references collections(id) ON DELETE CASCADE, 28 | foreign key(collection_element_id) references collection_elements(id) ON DELETE CASCADE 29 | ); 30 | 31 | CREATE TABLE IF NOT EXISTS explored_caches ( 32 | id INTEGER PRIMARY KEY AUTOINCREMENT, 33 | path TEXT NOT NULL, 34 | UNIQUE(path) 35 | ); 36 | 37 | CREATE TABLE IF NOT EXISTS all_game_caches ( 38 | id INTEGER PRIMARY KEY, 39 | gamename TEXT NOT NULL, 40 | thumbnail_url TEXT NOT NULL, 41 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP 42 | ); 43 | 44 | CREATE TABLE IF NOT EXISTS collection_element_details ( 45 | id INTEGER PRIMARY KEY AUTOINCREMENT, 46 | collection_element_id INTEGER NOT NULL, 47 | gamename_ruby TEXT NOT NULL, 48 | sellday TEXT NOT NULL, 49 | is_nukige INTEGER NOT NULL, 50 | brandname TEXT NOT NULL, 51 | brandname_ruby TEXT NOT NULL, 52 | foreign key(collection_element_id) references collection_elements(id) ON DELETE CASCADE 53 | ); 54 | -------------------------------------------------------------------------------- /src-tauri/src/migrations/V2__thumbnail_size.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE collection_elements ADD COLUMN thumbnail_width INTEGER; 2 | ALTER TABLE collection_elements ADD COLUMN thumbnail_height INTEGER; -------------------------------------------------------------------------------- /src-tauri/src/usecase/all_game_cache.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use chrono::{DateTime, Local}; 4 | use derive_new::new; 5 | 6 | use crate::{ 7 | domain::{ 8 | all_game_cache::{AllGameCache, AllGameCacheOneWithThumbnailUrl, NewAllGameCacheOne}, 9 | repository::all_game_cache::AllGameCacheRepository, 10 | }, 11 | infrastructure::repositoryimpl::repository::RepositoriesExt, 12 | }; 13 | 14 | #[derive(new)] 15 | pub struct AllGameCacheUseCase { 16 | repositories: Arc, 17 | } 18 | 19 | impl AllGameCacheUseCase { 20 | pub async fn get(&self, id: i32) -> anyhow::Result> { 21 | Ok(self 22 | .repositories 23 | .all_game_cache_repository() 24 | .get_by_ids(vec![id]) 25 | .await? 26 | .first() 27 | .and_then(|v| Some(v.clone()))) 28 | } 29 | pub async fn get_by_ids( 30 | &self, 31 | ids: Vec, 32 | ) -> anyhow::Result> { 33 | self.repositories 34 | .all_game_cache_repository() 35 | .get_by_ids(ids) 36 | .await 37 | } 38 | pub async fn get_all_game_cache(&self) -> anyhow::Result { 39 | self.repositories 40 | .all_game_cache_repository() 41 | .get_all() 42 | .await 43 | } 44 | pub async fn get_cache_last_updated(&self) -> anyhow::Result<(i32, DateTime)> { 45 | self.repositories 46 | .all_game_cache_repository() 47 | .get_last_updated() 48 | .await 49 | } 50 | pub async fn update_all_game_cache( 51 | &self, 52 | cache: Vec, 53 | ) -> anyhow::Result<()> { 54 | if cache.is_empty() { 55 | return Ok(()); 56 | } 57 | self.repositories 58 | .all_game_cache_repository() 59 | .delete_by_ids(cache.iter().map(|v| v.id).collect()) 60 | .await?; 61 | self.repositories 62 | .all_game_cache_repository() 63 | .update(cache) 64 | .await 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum UseCaseError { 5 | #[error("コレクションが存在しません")] 6 | CollectionIsNotFound, 7 | #[error("このコレクションは削除できません")] 8 | CollectionNotPermittedToDelete, 9 | #[error("コレクションはすでに存在しています")] 10 | CollectionIsAlreadyExist, 11 | #[error("コレクションエレメントが存在しません")] 12 | CollectionElementIsNotFound, 13 | #[error("`{0}`に有効な実行ファイルが存在しません")] 14 | IsNotValidPath(String), 15 | } 16 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/explored_cache.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use derive_new::new; 4 | 5 | use crate::{ 6 | domain::{explored_cache::ExploredCache, repository::explored_cache::ExploredCacheRepository}, 7 | infrastructure::repositoryimpl::repository::RepositoriesExt, 8 | }; 9 | 10 | #[derive(new)] 11 | pub struct ExploredCacheUseCase { 12 | repositories: Arc, 13 | } 14 | 15 | impl ExploredCacheUseCase { 16 | pub async fn get_cache(&self) -> anyhow::Result { 17 | Ok(self 18 | .repositories 19 | .explored_cache_repository() 20 | .get_all() 21 | .await?) 22 | } 23 | pub async fn add_cache(&self, adding_path: Vec) -> anyhow::Result<()> { 24 | let before = self 25 | .repositories 26 | .explored_cache_repository() 27 | .get_all() 28 | .await?; 29 | let adding = adding_path 30 | .into_iter() 31 | .filter_map(|v| match before.contains(&v) { 32 | true => None, 33 | false => Some(v), 34 | }) 35 | .collect(); 36 | Ok(self 37 | .repositories 38 | .explored_cache_repository() 39 | .add(adding) 40 | .await?) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/file_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use std::{collections::HashMap, sync::Arc}; 4 | 5 | use crate::{ 6 | domain::all_game_cache::AllGameCacheOne, infrastructure::explorerimpl::explorer::Explorers, 7 | usecase::file::FileUseCase, 8 | }; 9 | 10 | fn get_use_case() -> FileUseCase { 11 | FileUseCase::new(Arc::new(Explorers::new())) 12 | } 13 | 14 | const GAMENAME: &str = "gamename"; 15 | 16 | fn get_base_cache() -> AllGameCacheOne { 17 | AllGameCacheOne::new(1, GAMENAME.to_string()) 18 | } 19 | 20 | fn get_filepath(filename_without_extenstion: &str) -> String { 21 | format!("/path/to/{}.exe", filename_without_extenstion) 22 | } 23 | 24 | #[test] 25 | // 編集距離に基づいてupdate 26 | fn test_get_map_of_one_filepath_per_game_update_by_distance() { 27 | let u = get_use_case(); 28 | let expect_filepath = get_filepath(GAMENAME); 29 | let input = vec![ 30 | (get_base_cache(), expect_filepath.clone()), 31 | ( 32 | get_base_cache(), 33 | get_filepath(format!("{}11", GAMENAME).as_str()), 34 | ), 35 | ]; 36 | let mut expected_output: HashMap = HashMap::new(); 37 | expected_output.insert(get_base_cache().id, expect_filepath); 38 | let actual = u.get_map_of_one_filepath_per_game(input); 39 | assert_eq!(actual, expected_output); 40 | } 41 | 42 | #[test] 43 | // ignore_word に基づいてupdate 44 | fn test_get_map_of_one_filepath_per_game_update_by_ignore_word() { 45 | let u = get_use_case(); 46 | let expect_filepath = get_filepath("まったく関係のない名前あああああああ"); 47 | let input = vec![ 48 | (get_base_cache(), expect_filepath.clone()), 49 | ( 50 | get_base_cache(), 51 | // 編集距離を近づけるために GAMENAME を入れてる 52 | get_filepath(format!("{}-{}", GAMENAME, "ファイル設定").as_str()), 53 | ), 54 | ]; 55 | let mut expected_output: HashMap = HashMap::new(); 56 | expected_output.insert(get_base_cache().id, expect_filepath); 57 | let actual = u.get_map_of_one_filepath_per_game(input); 58 | assert_eq!(actual, expected_output); 59 | } 60 | 61 | #[test] 62 | // should_update_word に基づいてupdate 63 | fn test_get_map_of_one_filepath_per_game_update_by_should_update_word() { 64 | let u = get_use_case(); 65 | let expect_filepath = get_filepath("実行"); 66 | let input = vec![ 67 | (get_base_cache(), expect_filepath.clone()), 68 | ( 69 | get_base_cache(), 70 | // 編集距離を近づけるために GAMENAME を入れてる 71 | get_filepath(GAMENAME), 72 | ), 73 | ]; 74 | let mut expected_output: HashMap = HashMap::new(); 75 | expected_output.insert(get_base_cache().id, expect_filepath); 76 | let actual = u.get_map_of_one_filepath_per_game(input); 77 | assert_eq!(actual, expected_output); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod all_game_cache; 2 | pub mod collection; 3 | pub mod error; 4 | pub mod explored_cache; 5 | pub mod file; 6 | mod file_test; 7 | pub mod models; 8 | pub mod network; 9 | pub mod process; 10 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/models/collection.rs: -------------------------------------------------------------------------------- 1 | use derive_new::new; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::domain::{ 5 | collection::{NewCollection, NewCollectionElementDetail}, 6 | Id, 7 | }; 8 | 9 | #[derive(new, Debug, Clone, Serialize, Deserialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct CreateCollectionElementDetail { 12 | pub collection_element_id: i32, 13 | pub gamename_ruby: String, 14 | pub brandname: String, 15 | pub brandname_ruby: String, 16 | pub sellday: String, 17 | pub is_nukige: bool, 18 | } 19 | 20 | impl From for NewCollectionElementDetail { 21 | fn from(c: CreateCollectionElementDetail) -> Self { 22 | NewCollectionElementDetail::new( 23 | Id::new(c.collection_element_id), 24 | c.gamename_ruby, 25 | c.brandname, 26 | c.brandname_ruby, 27 | c.sellday, 28 | c.is_nukige, 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/models/file.rs: -------------------------------------------------------------------------------- 1 | use crate::domain::all_game_cache::AllGameCacheOne; 2 | 3 | pub struct Metadata { 4 | pub exe_path: String, 5 | pub icon_path: String, 6 | } 7 | 8 | pub struct NewElementContext { 9 | pub metadata: Metadata, 10 | pub game_cache: AllGameCacheOne, 11 | pub lnk_path: Option, 12 | pub exe_path: Option, 13 | } 14 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod collection; 2 | pub mod file; 3 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/network.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use derive_new::new; 4 | 5 | use crate::{ 6 | domain::{explorer::network::NetworkExplorer, network::ErogamescapeIDNamePair}, 7 | infrastructure::explorerimpl::explorer::ExplorersExt, 8 | }; 9 | 10 | #[derive(new)] 11 | pub struct NetworkUseCase { 12 | explorers: Arc, 13 | } 14 | 15 | impl NetworkUseCase { 16 | pub async fn get_all_games(&self) -> anyhow::Result> { 17 | Ok(self.explorers.network_explorer().get_all_games().await?) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src-tauri/src/usecase/process.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use derive_new::new; 4 | 5 | use crate::{ 6 | domain::windows::process::ProcessWindows, infrastructure::windowsimpl::windows::WindowsExt, 7 | }; 8 | 9 | #[derive(new)] 10 | pub struct ProcessUseCase { 11 | windows: Arc, 12 | } 13 | 14 | impl ProcessUseCase { 15 | pub async fn save_screenshot_by_pid( 16 | &self, 17 | process_id: u32, 18 | filepath: &str, 19 | ) -> anyhow::Result<()> { 20 | self.windows 21 | .process() 22 | .save_screenshot_by_process_id(process_id, &filepath) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "npm run dev", 4 | "beforeBuildCommand": "npm run build", 5 | "frontendDist": "../dist", 6 | "devUrl": "http://localhost:1420" 7 | }, 8 | "bundle": { 9 | "active": true, 10 | "targets": "all", 11 | "windows": { 12 | "wix": { 13 | "language": "ja-JP" 14 | } 15 | }, 16 | "icon": [ 17 | "icons/32x32.png", 18 | "icons/128x128.png", 19 | "icons/128x128@2x.png", 20 | "icons/icon.icns", 21 | "icons/icon.ico" 22 | ], 23 | "externalBin": [ 24 | "bin/extract-icon" 25 | ], 26 | "createUpdaterArtifacts": "v1Compatible" 27 | }, 28 | "productName": "Launcherg", 29 | "version": "0.3.3", 30 | "identifier": "ryoha.moe", 31 | "plugins": { 32 | "updater": { 33 | "endpoints": [ 34 | "https://raw.githubusercontent.com/ryoha000/launcherg/main/.tauri-updater.json" 35 | ], 36 | "windows": { 37 | "installMode": "passive", 38 | "installerArgs": [] 39 | }, 40 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDg3RDI3NjI1MjM0MkI0NDMKUldSRHRFSWpKWGJTaCtEM1JHNEhPZGlIdzhnSWdMc1I0Unp2SXZ1NEl2Q0FmeU9QOFUxaUZuU3AK" 41 | } 42 | }, 43 | "app": { 44 | "withGlobalTauri": false, 45 | "windows": [ 46 | { 47 | "fullscreen": false, 48 | "resizable": true, 49 | "title": "Launcherg", 50 | "width": 800, 51 | "height": 600 52 | } 53 | ], 54 | "security": { 55 | "assetProtocol": { 56 | "scope": [ 57 | "**" 58 | ], 59 | "enable": true 60 | }, 61 | "csp": null 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | {#await setDetailPromise then _} 21 | 22 | 23 | 24 | {/await} 25 | 26 |
27 | -------------------------------------------------------------------------------- /src/components/Home/ImportDropFiles.svelte: -------------------------------------------------------------------------------- 1 | 67 | 68 | {#if isOpenImportFileDrop && importFileDropPathIndex !== -1 && importFileDropPaths.length} 69 | importManually(e.detail)} 74 | on:cancel={skipImport} 75 | /> 76 | {/if} 77 | -------------------------------------------------------------------------------- /src/components/Home/ZappingGameItem.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | -------------------------------------------------------------------------------- /src/components/Sidebar/CollectionElement.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /src/components/Sidebar/CollectionElements.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {#if collectionElement.length} 16 |
17 |
18 |
19 | {#each collectionElement as { label, elements } (label)} 20 | 21 | {#each elements as ele (ele.id)} 22 | 23 | {/each} 24 | 25 | {/each} 26 |
27 |
28 |
29 | {/if} 30 |
31 | -------------------------------------------------------------------------------- /src/components/Sidebar/Header.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 |
11 | launcherg icon 12 | 13 |
14 |
15 | showSidebar.set(false)} 17 | appendClass="ml-auto border-0px p-1 bg-transparent" 18 | tooltip={{ 19 | content: "サイドバーを閉じる", 20 | placement: "bottom", 21 | theme: "default", 22 | delay: 1000, 23 | }} 24 | > 25 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /src/components/Sidebar/ImportManually.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 | (isOpen = false)} 52 | on:cancel={() => dispather("cancel")} 53 | title="Manually import game" 54 | confirmText="Import" 55 | {cancelText} 56 | confirmDisabled={!idInput || (!path && withInputPath)} 57 | on:confirm={onConfirm} 58 | > 59 |
60 | {#if withInputPath} 61 | (path = e.detail.value)} 66 | /> 67 | {/if} 68 |
69 | (idInput = e.detail.value)} 74 | /> 75 | {#if candidates.length !== 0} 76 |
77 |

候補

78 |
79 | {#each candidates as [id, gamename] (id)} 80 | 92 | {/each} 93 |
94 |
95 | {/if} 96 |
97 |
98 |
99 | -------------------------------------------------------------------------------- /src/components/Sidebar/ImportPopover.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
16 |
Select adding game option
17 |
22 | { 25 | dispatcher("close"); 26 | dispatcher("startAuto"); 27 | }} 28 | /> 29 | { 32 | dispatcher("close"); 33 | dispatcher("startManual"); 34 | }} 35 | /> 36 |
37 | -------------------------------------------------------------------------------- /src/components/Sidebar/MinimalSidebar.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | showSidebar.set(true)} 9 | appendClass="border-0px p-1 bg-transparent" 10 | tooltip={{ 11 | content: "サイドバーを開く", 12 | placement: "bottom", 13 | theme: "default", 14 | delay: 1000, 15 | }} 16 | > 17 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /src/components/Sidebar/Search.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 |
42 |
43 |
44 | 48 |
49 | 50 | 60 |
63 | 64 | close(null)} /> 65 | 66 |
67 |
68 | onScroll(e.detail.event)} 70 | bind:this={scrollable} 71 | > 72 |
73 | {#each attributes as attribute (attribute.key)} 74 | 77 | dispatcher("toggleAttributeEnabled", { key: attribute.key })} 78 | /> 79 | {/each} 80 |
81 |
82 | scrollable.scrollBy({ left: -100, behavior: "smooth" })} 87 | /> 88 | scrollable.scrollBy({ left: 100, behavior: "smooth" })} 92 | /> 93 |
94 |
95 | -------------------------------------------------------------------------------- /src/components/Sidebar/SearchAttribute.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /src/components/Sidebar/SearchAttrributeControl.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if show} 10 |
16 | 24 |
25 | {/if} 26 | -------------------------------------------------------------------------------- /src/components/Sidebar/SearchInput.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | { 10 | if (e.key === "/") { 11 | const active = document.activeElement; 12 | if ( 13 | active && 14 | (active.tagName === "input" || active.tagName === "textarea") 15 | ) { 16 | return; 17 | } 18 | setTimeout(() => { 19 | if (input) { 20 | input.focus(); 21 | } 22 | }, 20); 23 | } 24 | }} 25 | /> 26 |
29 |
32 | 46 |
47 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 |
49 | {#if $showSidebar} 50 |
51 |
54 |
55 | 56 |
57 | toggleEnable(e.detail.key)} 62 | /> 63 |
64 |
65 | 66 |
67 |
68 |
69 | {:else} 70 |
71 | 72 |
73 | {/if} 74 |
75 | -------------------------------------------------------------------------------- /src/components/Sidebar/SortPopover.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 |
25 |
Select sort option
26 |
31 | {#each sortOrders as sortOrder (sortOrder)} 32 | { 34 | value = sortOrder; 35 | dispatcher("close"); 36 | }} 37 | selected={value === sortOrder} 38 | text={SORT_LABELS[sortOrder]} 39 | showIcon 40 | /> 41 | {/each} 42 |
43 | -------------------------------------------------------------------------------- /src/components/Sidebar/SubHeader.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 |
31 | 登録したゲーム 32 |
33 | 34 |
47 | {#if isOpenImportAutomatically} 48 | 49 | {/if} 50 | {#if isOpenImportManually} 51 | importManually(e.detail)} 54 | on:cancel={() => (isOpenImportManually = false)} 55 | /> 56 | {/if} 57 | -------------------------------------------------------------------------------- /src/components/Sidebar/search.ts: -------------------------------------------------------------------------------- 1 | import { type SortOrder, sort } from "@/components/Sidebar/sort"; 2 | import type { CollectionElementsWithLabel } from "@/lib/types"; 3 | import { sidebarCollectionElements } from "@/store/sidebarCollectionElements"; 4 | import type { Option } from "@/lib/trieFilter"; 5 | import { 6 | FILTER_BY_ATTRIBUTE, 7 | type Attribute, 8 | } from "@/components/Sidebar/searchAttributes"; 9 | 10 | export const search = ( 11 | filteredOption: Option[], 12 | attributes: Attribute[], 13 | order: SortOrder 14 | ): CollectionElementsWithLabel[] => { 15 | const filteredElements = sidebarCollectionElements 16 | .value() 17 | .filter( 18 | (element) => 19 | filteredOption.findIndex((option) => option.value === element.id) !== -1 20 | ); 21 | 22 | const filtered = attributes.reduce( 23 | (acc, cur) => (cur.enabled ? FILTER_BY_ATTRIBUTE[cur.key](acc) : acc), 24 | filteredElements 25 | ); 26 | 27 | return sort(filtered, order); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Sidebar/searchAttributes.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionElement } from "@/lib/types"; 2 | import { createLocalStorageWritable } from "@/lib/utils"; 3 | 4 | export const ATTRIBUTES = { 5 | NUKIGE: "nukige", 6 | NOT_NUKIGE: "not_nukige", 7 | LIKE: "like", 8 | EXIST_PATH: "exist_path", 9 | } as const; 10 | 11 | export const ATTRIBUTE_LABELS: { [key in AttributeKey]: string } = { 12 | nukige: "抜きゲー", 13 | not_nukige: "非抜きゲー", 14 | like: "お気に入り", 15 | exist_path: "パスが存在", 16 | } as const; 17 | 18 | export type AttributeKey = (typeof ATTRIBUTES)[keyof typeof ATTRIBUTES]; 19 | const INITIAL_ATTRIBUTES = Object.values(ATTRIBUTES).map((v) => ({ 20 | key: v, 21 | enabled: false, 22 | })); 23 | 24 | export type Attribute = { key: AttributeKey; enabled: boolean }; 25 | 26 | export const searchAttributes = () => { 27 | const [attributes, getAttributes] = createLocalStorageWritable( 28 | "search-attributes", 29 | INITIAL_ATTRIBUTES 30 | ); 31 | 32 | const toggleEnable = (key: AttributeKey) => { 33 | attributes.update((attrs) => { 34 | const prevIndex = attrs.findIndex((v) => v.key === key); 35 | if (prevIndex < 0) { 36 | const val = { key, enabled: true }; 37 | // enable: true の最後に追加 38 | const index = attrs.findLastIndex((v) => v.enabled); 39 | if (index < 0) { 40 | return [val, ...attrs]; 41 | } 42 | return [...attrs.slice(0, index), val, ...attrs.slice(index)]; 43 | } 44 | 45 | const val = { key, enabled: !attrs[prevIndex].enabled }; 46 | const removedSelfAttrs = [ 47 | ...attrs.slice(0, prevIndex), 48 | ...attrs.slice(prevIndex + 1), 49 | ]; 50 | // enable: true の最後に追加 51 | const index = removedSelfAttrs.findLastIndex((v) => v.enabled); 52 | if (index < 0) { 53 | return [val, ...removedSelfAttrs]; 54 | } 55 | return [ 56 | ...removedSelfAttrs.slice(0, index + 1), 57 | val, 58 | ...removedSelfAttrs.slice(index + 1), 59 | ]; 60 | }); 61 | }; 62 | 63 | return { 64 | attributes: { 65 | subscribe: attributes.subscribe, 66 | }, 67 | toggleEnable, 68 | }; 69 | }; 70 | 71 | export const FILTER_BY_ATTRIBUTE: { 72 | [key in AttributeKey]: (src: CollectionElement[]) => CollectionElement[]; 73 | } = { 74 | nukige: (src) => src.filter((v) => v.isNukige), 75 | not_nukige: (src) => src.filter((v) => !v.isNukige), 76 | exist_path: (src) => src.filter((v) => v.installAt), 77 | like: (src) => src.filter((v) => v.likeAt), 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/Tab/ATab.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 |
push(`/${tab.type}/${tab.workId}`)} 25 | on:mousedown={closeWheelClick} 26 | > 27 |
32 |
33 |
38 | {tab.title} 39 |
40 |
43 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /src/components/Tab/ATabList.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |
9 |
10 | {#each $tabs as tab, i (tab.id)} 11 | 12 | {/each} 13 |
14 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /src/components/UI/ADisclosure.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 17 |
18 |
{label}
19 |
23 |
24 | 25 | {#if open} 26 |
27 | 28 | 29 | 30 |
31 | {/if} 32 | 33 | -------------------------------------------------------------------------------- /src/components/UI/APopover.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | {#if open} 18 |
22 | 23 | 24 | 25 |
26 | {/if} 27 |
28 | -------------------------------------------------------------------------------- /src/components/UI/Button.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 35 | {#if leftIcon} 36 |
37 | {/if} 38 | {#if text} 39 |
42 | {text} 43 |
44 | {/if} 45 | {#if rightIcon} 46 |
47 | {/if} 48 | 49 | -------------------------------------------------------------------------------- /src/components/UI/ButtonBase.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 | 66 | -------------------------------------------------------------------------------- /src/components/UI/ButtonCancel.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
45 | -------------------------------------------------------------------------------- /src/components/UI/LinkButton.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /src/components/UI/LinkText.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /src/components/UI/Modal.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 31 |
32 |
35 |
36 | {title} 37 |
38 | 45 |
46 |
47 | 48 |
49 | {#if withFooter} 50 | 51 |
52 |
53 |
61 |
62 |
63 | {/if} 64 |
65 |
66 | -------------------------------------------------------------------------------- /src/components/UI/ModalBase.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | dispatcher("close")}> 20 | 28 |
29 |
30 | 33 |
37 |
40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /src/components/UI/OptionButton.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /src/components/UI/QRCodeCanvas.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/UI/ScrollableHorizontal.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 49 | 50 |
51 |
(isHover = true)} 55 | on:mouseleave={() => (isHover = false)} 56 | > 57 | 58 |
59 |
60 | -------------------------------------------------------------------------------- /src/components/UI/Select.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 |
23 | 24 | 38 | 39 |
40 | { 50 | close(null); 51 | dispather("create"); 52 | }} 53 | on:close={() => close(null)} 54 | /> 55 |
56 | -------------------------------------------------------------------------------- /src/components/UI/SelectOptions.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 | {#if title} 38 |
39 |
42 | {title} 43 |
44 |
45 | {/if} 46 | {#if enableFilter} 47 |
48 | 49 |
50 | {/if} 51 |
52 | {#each $filtered as option, i (option)} 53 | 77 | {/each} 78 |
79 | {#if bottomCreateButtonText} 80 | 89 | {/if} 90 |
91 | -------------------------------------------------------------------------------- /src/components/UI/Table.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 |
{title}
13 |
14 |
15 | {#each rows as row (row.label)} 16 |
19 | {row.label} 20 |
21 | {#if row.component} 22 |
23 | 24 |
25 | {:else} 26 |
29 | {row.value} 30 |
31 | {/if} 32 | {/each} 33 |
34 |
35 | -------------------------------------------------------------------------------- /src/components/UI/VirtualScroller.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 | 21 |
22 |
27 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /src/components/UI/VirtualScrollerMasonry.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 |
38 | {#each $visibleLayouts as { top, left, width, height, element } (element.id)} 39 |
43 | 44 |
45 | {/each} 46 |
47 | -------------------------------------------------------------------------------- /src/components/UI/button.ts: -------------------------------------------------------------------------------- 1 | type Variant = "normal" | "accent" | "error" | "success"; 2 | -------------------------------------------------------------------------------- /src/components/UI/virtualScroller.ts: -------------------------------------------------------------------------------- 1 | import { derived, get, writable } from "svelte/store"; 2 | 3 | export const useVirtualScroller = () => { 4 | const virtualHeight = writable(0); 5 | const setVirtualHeight = (value: number) => virtualHeight.set(value); 6 | 7 | const scrollY = writable(0); 8 | const containerHeight = writable(0); 9 | let containerNode: HTMLElement | null = null; 10 | 11 | const headerHeight = writable(0); 12 | 13 | const contentsWidth = writable(0); 14 | 15 | const contentsScrollY = derived< 16 | [typeof scrollY, typeof headerHeight], 17 | number 18 | >([scrollY, headerHeight], ([$scrollY, $headerHeight], set) => { 19 | set(Math.max(0, $scrollY - $headerHeight)); 20 | }); 21 | 22 | let notAppliedContentsScrollY = 0; 23 | const contentsScrollTo = (y: number) => { 24 | const to = y + get(headerHeight); 25 | if (!containerNode || containerNode.scrollHeight < to) { 26 | notAppliedContentsScrollY = y; 27 | return; 28 | } 29 | containerNode.scrollTo({ top: to }); 30 | notAppliedContentsScrollY = 0; 31 | }; 32 | 33 | virtualHeight.subscribe((v) => { 34 | if (notAppliedContentsScrollY) { 35 | contentsScrollTo(notAppliedContentsScrollY); 36 | } 37 | }); 38 | 39 | const header = (node: HTMLElement) => { 40 | const resizeObserver = new ResizeObserver((entries) => { 41 | const entry = entries[0]; 42 | if (!entry) { 43 | return; 44 | } 45 | headerHeight.set(entry.contentRect.height); 46 | }); 47 | resizeObserver.observe(node); 48 | 49 | return { 50 | destroy() { 51 | resizeObserver.disconnect(); 52 | }, 53 | }; 54 | }; 55 | const contents = (node: HTMLElement) => { 56 | const resizeObserver = new ResizeObserver((entries) => { 57 | const entry = entries[0]; 58 | if (!entry) { 59 | return; 60 | } 61 | contentsWidth.set(entry.contentRect.width); 62 | }); 63 | resizeObserver.observe(node); 64 | 65 | return { 66 | destroy() { 67 | resizeObserver.disconnect(); 68 | }, 69 | }; 70 | }; 71 | 72 | const container = (node: HTMLElement) => { 73 | containerNode = node; 74 | if (notAppliedContentsScrollY) { 75 | contentsScrollTo(notAppliedContentsScrollY); 76 | } 77 | const onScroll = (e: Event) => { 78 | const target = e.target; 79 | if (!(target instanceof HTMLElement)) { 80 | return; 81 | } 82 | scrollY.set(target.scrollTop); 83 | }; 84 | node.addEventListener("scroll", onScroll); 85 | 86 | const resizeObserver = new ResizeObserver((entries) => { 87 | const entry = entries[0]; 88 | if (!entry) { 89 | return; 90 | } 91 | containerHeight.set(entry.contentRect.height); 92 | }); 93 | resizeObserver.observe(node); 94 | 95 | return { 96 | destroy() { 97 | containerNode = null; 98 | node.removeEventListener("scroll", onScroll); 99 | resizeObserver.disconnect(); 100 | }, 101 | }; 102 | }; 103 | 104 | return { 105 | container, 106 | header, 107 | contents, 108 | virtualHeight, 109 | setVirtualHeight, 110 | contentsWidth, 111 | contentsScrollY, 112 | containerHeight, 113 | contentsScrollTo, 114 | }; 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/Work/DeleteElement.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | (isOpen = false)} 23 | on:cancel={() => (isOpen = false)} 24 | title={`Delete game`} 25 | withContentPadding={false} 26 | autofocusCloseButton 27 | headerClass="border-b-(border-warning opacity-40) " 28 | > 29 |
32 |
35 |
36 |
37 | このゲームの登録を削除します 38 |
39 |
40 | 参照元のファイルが消えることはありません。プレイ時間のデータは同じゲームを登録したとき引き継がれます。 41 |
42 |
43 |
44 |
45 |
53 | 54 | -------------------------------------------------------------------------------- /src/components/Work/Detail.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 |
31 | 32 |
33 | {#each work.creators.writers as v (v.id)} 34 | 35 | {/each} 36 |
37 |
38 | {#if work.creators.illustrators.length} 39 | 40 |
41 | {#each work.creators.illustrators as v (v.id)} 42 | 43 | {/each} 44 |
45 |
46 | {/if} 47 | {#if work.creators.voiceActors.length} 48 | 49 |
50 | {#each work.creators.voiceActors as v (v.id)} 51 |
52 | 53 |
{v.role}
54 |
55 | {/each} 56 |
57 |
58 | {/if} 59 | {#if work.musics.length} 60 | 61 |
62 | {#each work.musics as title, i (`${title}-${i}`)} 63 |
64 | 69 |
70 |
71 | {title} 72 |
73 | 74 |
75 | {/each} 76 |
77 | 78 | {/if} 79 |
80 |
81 | -------------------------------------------------------------------------------- /src/components/Work/DetailRow.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
11 | {label} 12 |
13 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /src/components/Work/LinkToSidebar.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/Work/OtherInfomationSection.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
{label}
8 |
9 | {value} 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/components/Work/OtherInformation.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | (isOpen = false)} 22 | on:cancel={() => (isOpen = false)} 23 | title={`Infomation`} 24 | autofocusCloseButton 25 | withFooter={false} 26 | > 27 |
28 | 29 | 30 | 34 | 35 | {#await gameCache then c} 36 | 37 | {/await} 38 |
39 |
40 | -------------------------------------------------------------------------------- /src/components/Work/PlayButton.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
22 | { 25 | dispatcher("close"); 26 | dispatcher("playAdmin"); 27 | }} 28 | /> 29 | { 32 | dispatcher("close"); 33 | dispatcher("play"); 34 | }} 35 | /> 36 |
37 | -------------------------------------------------------------------------------- /src/components/Work/QRCode.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | (isOpen = false)} 27 | on:cancel={() => (isOpen = false)} 28 | title="Link to Smartphone" 29 | autofocusCloseButton 30 | withFooter={false} 31 | > 32 |
33 |
34 | QRコードを読み込む、またはリンクを共有することでほかの端末からメモを取れます 35 |
36 | {#if readyPromise} 37 | {#await readyPromise} 38 |
39 |
42 |
処理中
43 |
44 | {:then value} 45 |
46 | 55 | 56 |
57 | {/await} 58 | {/if} 59 |
60 | 61 | -------------------------------------------------------------------------------- /src/components/Work/SettingPopover.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
18 |
Select game option
19 |
24 | { 27 | dispatcher("close"); 28 | dispatcher("selectOpen"); 29 | }} 30 | /> 31 | { 34 | dispatcher("close"); 35 | dispatcher("selectChange"); 36 | }} 37 | /> 38 | { 41 | dispatcher("close"); 42 | dispatcher("selectDelete"); 43 | }} 44 | /> 45 | { 48 | dispatcher("close"); 49 | dispatcher("selectOtherInfomation"); 50 | }} 51 | /> 52 |
53 | -------------------------------------------------------------------------------- /src/components/Work/Work.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 | {#key work.imgUrl} 14 | 15 | {/key} 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/components/Work/WorkImage.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {name}_icon 7 | -------------------------------------------------------------------------------- /src/components/Work/WorkLayout.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if isLandscape} 20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 | {:else} 28 |
29 |
32 | 33 | 34 |
35 | 36 |
37 | {/if} 38 | -------------------------------------------------------------------------------- /src/components/Work/WorkMain.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 |
{work.name}
27 | {#await seiyaUrlPromise then seiyaUrl} 28 | 29 | {/await} 30 |
31 | 32 | 37 | {#await seiyaUrlPromise then url} 38 | 39 | {/await} 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | overflow: hidden; 5 | margin: 0; 6 | padding: 0; 7 | 8 | ::-webkit-scrollbar { 9 | width: 0.4rem; 10 | height: 0.3rem; 11 | } 12 | ::-webkit-scrollbar-thumb { 13 | background-color: #CDD9E5; 14 | height: 3rem; 15 | width: 5rem; 16 | border-radius: 999px; 17 | cursor: pointer; 18 | } 19 | } 20 | 21 | #app { 22 | height: 100%; 23 | overflow: auto; 24 | /* scrollbar-gutter: stable; */ 25 | } 26 | 27 | a { 28 | color: inherit; 29 | background-color: transparent; 30 | text-decoration: none; 31 | } 32 | 33 | a:hover { 34 | text-decoration: none; 35 | } 36 | 37 | * { 38 | outline: none; 39 | } 40 | 41 | .tippy-box[data-theme~='default'] { 42 | > .tippy-arrow { 43 | color: #636e7b; 44 | } 45 | background-color: #636e7b; 46 | color: #c3cfdb; 47 | } 48 | 49 | .simplebar-vertical { 50 | > .simplebar-scrollbar::before { 51 | background-color: #CDD9E5; 52 | } 53 | } 54 | 55 | .simplebar-horizontal { 56 | height: 0.25rem !important; 57 | > .simplebar-scrollbar::before { 58 | background-color: #CDD9E5; 59 | bottom: 0; 60 | } 61 | } 62 | 63 | .hide-scrollbar { 64 | .simplebar-horizontal { 65 | height: 0 !important; 66 | } 67 | } 68 | 69 | #app { 70 | .EasyMDEContainer { 71 | display: flex; 72 | flex-direction: column; 73 | height: 100%; 74 | max-width: 100%; 75 | min-width: 0; 76 | width: 100%; 77 | } 78 | } 79 | .EasyMDEContainer > .editor-toolbar { 80 | border-color: transparent; 81 | border-bottom-width: 1px; 82 | border-bottom-color: #444c56; 83 | * { 84 | color: #adbac7; 85 | } 86 | i.separator { 87 | border-left: 1px solid #444c56; 88 | border-right: 1px solid #444c56; 89 | } 90 | button:hover { 91 | background-color: #2d333b; 92 | border-color: #444c56; 93 | } 94 | button.active { 95 | background-color: #2d333b; 96 | border-color: #444c56; 97 | } 98 | } 99 | .EasyMDEContainer > .cm-s-easymde { 100 | .cm-header { 101 | line-height: 160%; 102 | } 103 | .cm-header-1 { 104 | font-size: 1.75rem; 105 | } 106 | .cm-header-2 { 107 | font-size: 1.5rem; 108 | } 109 | .cm-header-3 { 110 | font-size: 1.25rem; 111 | } 112 | .cm-header-4 { 113 | font-size: 1.125rem; 114 | } 115 | .cm-header-5 { 116 | font-size: 1.08rem; 117 | } 118 | .cm-header-6 { 119 | font-size: 1.04rem; 120 | } 121 | .cm-image { 122 | font-size: 0.5rem; 123 | } 124 | .cm-url { 125 | font-size: 0.5rem; 126 | line-height: 120%; 127 | } 128 | } 129 | .CodeMirror.cm-s-easymde.CodeMirror-wrap { 130 | color: #adbac7; 131 | border-width: 0; 132 | border-color: #444c56; 133 | background-color: transparent; 134 | flex: 1; 135 | } 136 | .EasyMDEContainer .cm-s-easymde .CodeMirror-cursor { 137 | border-color: #adbac7; 138 | } 139 | 140 | .EasyMDEContainer .CodeMirror-selected { 141 | background: #2d333b !important; 142 | } 143 | 144 | .editor-toolbar { 145 | border-top: 1px solid #333; 146 | border-left: 1px solid #333; 147 | border-right: 1px solid #333; 148 | } 149 | 150 | .editor-toolbar.fullscreen { 151 | background: #000; 152 | } 153 | 154 | .editor-preview { 155 | background: #000; 156 | } 157 | -------------------------------------------------------------------------------- /src/layouts/Layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
11 | 12 |
13 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/lib/chunk.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "@tauri-apps/plugin-fs"; 2 | 3 | export const useChunk = () => { 4 | let currentChunkId = 0; 5 | // chunk id は頭につける都合上8bitで表現できるようにする 6 | const chunkIdMask = 0xff; 7 | const CHUNK_SIZE = 16 * 1024; // 16KB 8 | const CHUNK_HEADER_SIZE = 2; // [chunkId: 1byte][index: 1byte] 9 | const CHUNK_DATA_SIZE = CHUNK_SIZE - CHUNK_HEADER_SIZE; 10 | const createNewChunkId = () => { 11 | return (currentChunkId + 1) & chunkIdMask; 12 | }; 13 | 14 | const createChunks = async (filePath: string) => { 15 | // ファイルをバイナリとして読み込む 16 | const data = await readFile(filePath); 17 | const lowerCasePath = filePath.toLowerCase(); 18 | // MIME タイプを推定 (ここでは ".png" の場合 "image/png" としていますが、他の形式もサポートする場合は調整が必要) 19 | const mimeType = (function () { 20 | if (lowerCasePath.endsWith(".png")) return "image/png"; 21 | if (lowerCasePath.endsWith(".jpg") || lowerCasePath.endsWith(".jpeg")) 22 | return "image/jpeg"; 23 | if (lowerCasePath.endsWith(".gif")) return "image/gif"; 24 | if (lowerCasePath.endsWith(".webp")) return "image/webp"; 25 | throw new Error("Unsupported file type"); 26 | })(); 27 | const chunkId = createNewChunkId(); 28 | 29 | const totalChunkLength = Math.ceil(data.byteLength / CHUNK_DATA_SIZE); 30 | const chunkArray: Uint8Array[] = []; 31 | for (let i = 0; i < totalChunkLength; i++) { 32 | chunkArray[i] = new Uint8Array([ 33 | chunkId, 34 | i, 35 | ...data.slice( 36 | i * CHUNK_DATA_SIZE, 37 | Math.min((i + 1) * CHUNK_DATA_SIZE, data.byteLength) 38 | ), 39 | ]); 40 | } 41 | 42 | return [{ chunkId, mimeType, totalChunkLength }, chunkArray] as const; 43 | }; 44 | 45 | return { createChunks }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/lib/filter.ts: -------------------------------------------------------------------------------- 1 | export type Option = { label: string; value: T; otherLabels?: string[] }; 2 | 3 | import type { CollectionElement } from "@/lib/types"; 4 | import { writable, type Readable, type Writable } from "svelte/store"; 5 | import { toHiragana, toRomaji } from "wanakana"; 6 | 7 | export const collectionElementsToOptions = (elements: CollectionElement[]) => 8 | elements.map((v) => ({ 9 | label: v.gamename, 10 | value: v.id, 11 | otherLabels: [ 12 | toHiragana(v.gamenameRuby), 13 | toRomaji(v.gamenameRuby), 14 | v.brandname, 15 | toHiragana(v.brandnameRuby), 16 | toRomaji(v.brandnameRuby), 17 | ], 18 | })); 19 | 20 | export const useFilter = ( 21 | query: Writable, 22 | options: Readable[]>, 23 | getOptions: () => Option[] 24 | ) => { 25 | const filtered = writable[]>([...getOptions()]); 26 | 27 | const init = () => { 28 | const lazyQuery = writable(""); 29 | filtered.set([...getOptions()]); 30 | 31 | const optionMap = new Map["value"], Option>(); 32 | for (const option of getOptions()) { 33 | optionMap.set(option.value, option); 34 | } 35 | 36 | const cache: Record[]> = {}; 37 | 38 | let lazyQueryTimer: ReturnType | null = null; 39 | query.subscribe((_query) => { 40 | if (lazyQueryTimer) { 41 | clearTimeout(lazyQueryTimer); 42 | } 43 | lazyQueryTimer = setTimeout(() => { 44 | lazyQuery.set(_query.toLowerCase()); 45 | lazyQueryTimer = null; 46 | }, 200); 47 | }); 48 | lazyQuery.subscribe((_query) => { 49 | if (!_query) { 50 | return filtered.set([...getOptions()]); 51 | } 52 | const cached = Object.entries(cache).find(([input, _]) => 53 | _query.includes(input) 54 | ); 55 | const targetOptions = cached ? cached[1] : getOptions(); 56 | const _filtered = targetOptions.filter((option) => 57 | [option.label, ...(option.otherLabels ?? [])].find((key) => 58 | key.toLowerCase().includes(_query) 59 | ) 60 | ); 61 | cache[_query] = _filtered; 62 | filtered.set(_filtered); 63 | }); 64 | }; 65 | init(); 66 | 67 | options.subscribe(() => init()); 68 | 69 | return { query, filtered }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/lib/importManually.ts: -------------------------------------------------------------------------------- 1 | import { commandGetExePathByLnk, commandGetGameCacheById } from "@/lib/command"; 2 | import { scrapeSql } from "@/lib/scrapeSql"; 3 | import { showErrorToast } from "@/lib/toast"; 4 | 5 | export const useImportManually = () => { 6 | const parseErogameScapeId = (input: string) => { 7 | { 8 | const idNumber = +input; 9 | if (!isNaN(idNumber)) { 10 | return idNumber; 11 | } 12 | } 13 | 14 | try { 15 | const url = new URL(input); 16 | const idString = url.searchParams.get("game"); 17 | if (!idString) { 18 | return; 19 | } 20 | const idNumber = +idString; 21 | if (isNaN(idNumber)) { 22 | return; 23 | } 24 | return idNumber; 25 | } catch (e) { 26 | console.warn(e); 27 | } 28 | }; 29 | 30 | const getNewCollectionElementByInputs = async ( 31 | idInput: string, 32 | pathInput: string 33 | ) => { 34 | const id = parseErogameScapeId(idInput); 35 | if (!id) { 36 | return showErrorToast("ErogameScape の id として解釈できませんでした"); 37 | } 38 | 39 | const gameCache = await commandGetGameCacheById(id); 40 | if (!gameCache) { 41 | return showErrorToast( 42 | "存在しない id でした。ErogameScape を確認して存在していたらバグなので @ryoha000 に連絡していただけると幸いです。" 43 | ); 44 | } 45 | 46 | if (pathInput.toLowerCase().endsWith("exe")) { 47 | return { exePath: pathInput, lnkPath: null, gameCache }; 48 | } else { 49 | return { exePath: null, lnkPath: pathInput, gameCache }; 50 | } 51 | }; 52 | 53 | return { getNewCollectionElementByInputs }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/lib/registerCollectionElementDetails.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commandGetNotRegisterdDetailElementIds, 3 | commandCreateElementDetails, 4 | } from "@/lib/command"; 5 | import { scrapeSql } from "@/lib/scrapeSql"; 6 | 7 | export const registerCollectionElementDetails = async () => { 8 | const ids = await commandGetNotRegisterdDetailElementIds(); 9 | if (!ids.length) { 10 | return; 11 | } 12 | 13 | const query = `select gamelist.id, gamelist.furigana, gamelist.sellday, gamelist.okazu, brandlist.brandname, brandlist.brandfurigana from gamelist inner join brandlist on brandlist.id = gamelist.brandname where gamelist.id IN (${ids.join( 14 | ", " 15 | )});`; 16 | const rows = await scrapeSql(query, 6); 17 | await commandCreateElementDetails( 18 | rows.map((row) => ({ 19 | collectionElementId: +row[0], 20 | gamenameRuby: row[1], 21 | sellday: row[2], 22 | isNukige: row[3].includes("t"), 23 | brandname: row[4], 24 | brandnameRuby: row[5], 25 | })) 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/scrapeAllGame.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commandGetAllGameCacheLastUpdated, 3 | commandUpdateAllGameCache, 4 | } from "@/lib/command"; 5 | import { scrapeSql } from "@/lib/scrapeSql"; 6 | import type { AllGameCacheOne } from "@/lib/types"; 7 | import { fetch } from "@tauri-apps/plugin-http"; 8 | 9 | const STEP = 5000; 10 | const MAX_SCRAPE_COUNT = 20; 11 | 12 | const ALL_GAME_CACHE_BASE_QUERY = `SELECT id, gamename, CASE WHEN dmm_genre='digital' AND dmm_genre_2='pcgame' THEN 'https://pics.dmm.co.jp/digital/pcgame/' || dmm || '/' || dmm || 'pl.jpg' 13 | WHEN dmm_genre='digital' AND dmm_genre_2='doujin' THEN 'https://doujin-assets.dmm.co.jp/digital/game/' || dmm || '/' || dmm || 'pr.jpg' 14 | WHEN dmm_genre='mono' AND dmm_genre_2='pcgame' THEN 'https://pics.dmm.co.jp/mono/game/' || dmm || '/' || dmm || 'pl.jpg' 15 | WHEN dlsite_id IS NOT NULL AND (dlsite_domain='pro' OR dlsite_domain='soft') THEN 'https://img.dlsite.jp/modpub/images2/work/professional/' || left(dlsite_id,2) || LPAD(CAST(CAST(RIGHT(LEFT(dlsite_id, 5), 3) AS INTEGER) + 1 AS TEXT), 3, '0') || '000/' || dlsite_id || '_img_main.jpg' 16 | WHEN dlsite_id IS NOT NULL THEN 'https://img.dlsite.jp/modpub/images2/work/doujin/' || left(dlsite_id,2) || LPAD(CAST(CAST(RIGHT(LEFT(dlsite_id, 5), 3) AS INTEGER) + 1 AS TEXT), 3, '0') || '000/' || dlsite_id || '_img_main.jpg' 17 | WHEN dmm IS NOT NULL THEN 'https://pics.dmm.co.jp/mono/game/' || dmm || '/' || dmm || 'pl.jpg' 18 | WHEN surugaya_1 IS NOT NULL THEN 'https://www.suruga-ya.jp/database/pics/game/' || surugaya_1 || '.jpg' 19 | ELSE '' END AS thumbnail_url FROM gamelist`; 20 | 21 | export const scrapeAllGameCacheOnes = async (ids: number[]) => { 22 | const idGameNamePairs: AllGameCacheOne[] = []; 23 | for (let i = 0; i < ids.length; i += STEP) { 24 | const inQuery = ids.map((v) => `'${v}'`).join(","); 25 | const query = `${ALL_GAME_CACHE_BASE_QUERY} WHERE id IN (${inQuery});`; 26 | const rows = await scrapeSql(query, 3); 27 | idGameNamePairs.push( 28 | ...rows.map((v) => ({ id: +v[0], gamename: v[1], thumbnailUrl: v[2] })) 29 | ); 30 | await new Promise((resolve) => setTimeout(resolve, 2000)); 31 | } 32 | 33 | return idGameNamePairs; 34 | }; 35 | 36 | export const scrapeAllGame = async (idCursor = 0) => { 37 | const idGameNamePairs: AllGameCacheOne[] = []; 38 | for (let i = 0; i < MAX_SCRAPE_COUNT; i++) { 39 | const query = `${ALL_GAME_CACHE_BASE_QUERY} WHERE id >= ${idCursor} AND id < ${ 40 | idCursor + STEP 41 | } AND model = 'PC';`; 42 | const rows = await scrapeSql(query, 3); 43 | if (!rows.length) { 44 | console.log( 45 | `end within ${i + 1} loop. games.length: ${idGameNamePairs.length}` 46 | ); 47 | break; 48 | } 49 | idGameNamePairs.push( 50 | ...rows.map((v) => ({ id: +v[0], gamename: v[1], thumbnailUrl: v[2] })) 51 | ); 52 | await new Promise((resolve) => setTimeout(resolve, 2000)); 53 | idCursor += STEP; 54 | } 55 | 56 | return idGameNamePairs; 57 | }; 58 | 59 | export const initializeAllGameCache = async () => { 60 | let objValue: AllGameCacheOne[] = []; 61 | try { 62 | const lastUpdated = await commandGetAllGameCacheLastUpdated(); 63 | const now = new Date(); 64 | if (now.getTime() - lastUpdated.date.getTime() > 1000 * 60 * 60 * 24 * 1) { 65 | objValue = await scrapeAllGame(lastUpdated.id + 1); 66 | } 67 | } catch (e) { 68 | console.warn( 69 | "all_game_cache の取得に失敗しました。おそらく初期化されていないため初期化します。" 70 | ); 71 | console.warn(e); 72 | const response = await fetch( 73 | "https://raw.githubusercontent.com/ryoha000/launcherg/main/script/all_games.json", 74 | { method: "GET" } 75 | ); 76 | const initValue = (await response.json()) as AllGameCacheOne[]; 77 | const maxId = initValue.reduce( 78 | (acc, cur) => (acc > cur.id ? acc : cur.id), 79 | 0 80 | ); 81 | objValue = [...initValue, ...(await scrapeAllGame(maxId + 1))]; 82 | } 83 | await commandUpdateAllGameCache(objValue); 84 | }; 85 | -------------------------------------------------------------------------------- /src/lib/scrapeSeiya.ts: -------------------------------------------------------------------------------- 1 | import type { SeiyaDataPair } from "@/lib/types"; 2 | import { fetch } from "@tauri-apps/plugin-http"; 3 | import Encoding from "encoding-japanese"; 4 | 5 | export const getSeiyaDataPairs = async () => { 6 | const response = await fetch("https://seiya-saiga.com/game/kouryaku.html", { 7 | method: "GET", 8 | }); 9 | const data = await response.arrayBuffer(); 10 | const uint8array = new Uint8Array(data); 11 | const parser = new DOMParser(); 12 | const htmlUnicodeArray = Encoding.convert(uint8array, { 13 | to: "UNICODE", 14 | from: "SJIS", 15 | }); 16 | const html = Encoding.codeToString(htmlUnicodeArray); 17 | const doc = parser.parseFromString(html, "text/html"); 18 | 19 | const trs = doc.getElementsByTagName("tr"); 20 | 21 | const pairs: SeiyaDataPair[] = []; 22 | for (const tr of trs) { 23 | const atag = tr.getElementsByTagName("a")?.[0]; 24 | if (!atag) { 25 | continue; 26 | } 27 | 28 | const name = atag.innerHTML; 29 | const path = atag.getAttribute("href"); 30 | if (!name || !path) { 31 | continue; 32 | } 33 | 34 | let url = ""; 35 | if (path?.startsWith("http")) { 36 | url = path; 37 | } else { 38 | url = `https://seiya-saiga.com/game/${path}`; 39 | } 40 | 41 | pairs.push([name, url]); 42 | } 43 | return pairs; 44 | }; 45 | -------------------------------------------------------------------------------- /src/lib/scrapeSql.ts: -------------------------------------------------------------------------------- 1 | import { convertSpecialCharacters } from "@/lib/utils"; 2 | import { fetch } from "@tauri-apps/plugin-http"; 3 | 4 | export const scrapeSql = async (query: string, colNums: number) => { 5 | try { 6 | const formData = new FormData(); 7 | formData.append("sql", query); 8 | const response = await fetch( 9 | "https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/sql_for_erogamer_form.php", 10 | { 11 | method: "POST", 12 | body: formData, 13 | } 14 | ); 15 | const parser = new DOMParser(); 16 | const doc = parser.parseFromString(await response.text(), "text/html"); 17 | 18 | const rows: string[][] = []; 19 | doc.querySelectorAll("#query_result_main tr").forEach((tr, i) => { 20 | if (i === 0) { 21 | return; 22 | } 23 | const row: string[] = []; 24 | let isSkip = false; 25 | for (let index = 0; index < colNums; index++) { 26 | const scrapeIndex = index + 1; 27 | const col = tr.querySelector(`td:nth-child(${scrapeIndex})`); 28 | if (!col) { 29 | isSkip = true; 30 | break; 31 | } 32 | row.push(convertSpecialCharacters(col.innerHTML)); 33 | } 34 | if (isSkip) { 35 | return; 36 | } 37 | rows.push(row); 38 | }); 39 | return rows; 40 | } catch (e) { 41 | console.error(e); 42 | return []; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/lib/toast.ts: -------------------------------------------------------------------------------- 1 | import Toastify from "toastify-js"; 2 | 3 | export const showInfoToast = (text: string, duration = 3000) => { 4 | Toastify({ 5 | text, 6 | duration: duration, 7 | gravity: "bottom", // `top` or `bottom` 8 | position: "right", // `left`, `center` or `right` 9 | stopOnFocus: true, // Prevents dismissing of toast on hover 10 | style: { 11 | background: "#22272e", 12 | border: "1px solid #444c56", 13 | "border-radius": "0.5rem", 14 | }, 15 | }).showToast(); 16 | }; 17 | 18 | export const showErrorToast = (text: string) => { 19 | Toastify({ 20 | text, 21 | duration: 5000, 22 | gravity: "bottom", // `top` or `bottom` 23 | position: "right", // `left`, `center` or `right` 24 | stopOnFocus: true, // Prevents dismissing of toast on hover 25 | style: { 26 | background: "#EA4E60", 27 | border: "1px solid #EA4E60", 28 | color: "#FFFFFF", 29 | "border-radius": "0.5rem", 30 | }, 31 | }).showToast(); 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/trieFilter.ts: -------------------------------------------------------------------------------- 1 | export type Option = { label: string; value: T; otherLabels?: string[] }; 2 | import type { CollectionElement } from "@/lib/types"; 3 | import { isNotNullOrUndefined } from "@/lib/utils"; 4 | import { writable, type Readable } from "svelte/store"; 5 | import TrieSearch from "trie-search"; 6 | import { toHiragana, toRomaji } from "wanakana"; 7 | 8 | type KeyValue = { 9 | key: string; 10 | value: T; 11 | }; 12 | 13 | export const collectionElementsToOptions = (elements: CollectionElement[]) => 14 | elements.map((v) => ({ 15 | label: v.gamename, 16 | value: v.id, 17 | otherLabels: [ 18 | toHiragana(v.gamenameRuby), 19 | toRomaji(v.gamenameRuby), 20 | v.brandname, 21 | toHiragana(v.brandnameRuby), 22 | toRomaji(v.brandnameRuby), 23 | ], 24 | })); 25 | 26 | export const useTrieFilter = ( 27 | options: Readable[]>, 28 | getOptions: () => Option[] 29 | ) => { 30 | const query = writable(""); 31 | const filtered = writable[]>([...getOptions()]); 32 | 33 | const init = () => { 34 | query.set(""); 35 | filtered.set([...getOptions()]); 36 | 37 | const optionMap = new Map["value"], Option>(); 38 | for (const option of getOptions()) { 39 | optionMap.set(option.value, option); 40 | } 41 | 42 | const trie: TrieSearch> = new TrieSearch>("key"); 43 | for (const option of getOptions()) { 44 | trie.add({ key: option.label, value: option.value }); 45 | if (!option.otherLabels) { 46 | continue; 47 | } 48 | for (const otherLabel of option.otherLabels) { 49 | trie.add({ key: otherLabel, value: option.value }); 50 | } 51 | } 52 | 53 | query.subscribe((_query) => { 54 | if (!_query) { 55 | return filtered.set([...getOptions()]); 56 | } 57 | const searched = trie.search(_query); 58 | filtered.set( 59 | [...new Set(searched.map((v) => v.value))] 60 | .map((v) => optionMap.get(v)) 61 | .filter(isNotNullOrUndefined) 62 | ); 63 | }); 64 | }; 65 | init(); 66 | 67 | options.subscribe(() => init()); 68 | 69 | return { query, filtered }; 70 | }; 71 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Work = { 2 | id: number; 3 | name: string; 4 | brandId: number; 5 | brandName: string; 6 | officialHomePage: string; 7 | sellday: string; 8 | imgUrl: string; 9 | statistics: Statistics; 10 | creators: Creators; 11 | musics: string[]; 12 | }; 13 | 14 | export type Statistics = { 15 | median: number; 16 | average: number; 17 | count: number; 18 | playTime: string; 19 | }; 20 | 21 | export type Creators = { 22 | illustrators: Creator[]; 23 | writers: Creator[]; 24 | voiceActors: VoiceActor[]; 25 | }; 26 | 27 | export type Creator = { 28 | id: number; 29 | name: string; 30 | }; 31 | 32 | export const VoiceActorImportance = { 33 | Main: 0, 34 | Sub: 1, 35 | Mob: 2, 36 | } as const; 37 | 38 | export type VoiceActor = { 39 | role: string; 40 | importance: (typeof VoiceActorImportance)[keyof typeof VoiceActorImportance]; 41 | } & Creator; 42 | 43 | export type Collection = { 44 | id: number; 45 | name: string; 46 | }; 47 | 48 | export type CollectionElement = { 49 | id: number; // Work.id と同じ 50 | gamename: string; 51 | gamenameRuby: string; 52 | brandname: string; 53 | brandnameRuby: string; 54 | sellday: string; 55 | isNukige: boolean; 56 | installAt: string | null; 57 | lastPlayAt: string | null; 58 | likeAt: string | null; 59 | registeredAt: string; 60 | exePath: string; 61 | lnkPath: string; 62 | icon: string; 63 | thumbnail: string; 64 | thumbnailWidth: number | null; 65 | thumbnailHeight: number | null; 66 | }; 67 | 68 | export type CollectionElementsWithLabel = { 69 | label: string; 70 | elements: CollectionElement[]; 71 | }; 72 | 73 | export type SeiyaDataPair = [string, string]; 74 | 75 | export type CollectionElementDetail = { 76 | collectionElementId: number; 77 | gamenameRuby: string; 78 | brandname: string; 79 | brandnameRuby: string; 80 | sellday: string; 81 | isNukige: boolean; 82 | }; 83 | 84 | export type AllGameCacheOne = { 85 | id: number; 86 | gamename: string; 87 | thumbnailUrl: string; 88 | }; 89 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export function createWritable(initialValue: T) { 4 | let _value = initialValue; 5 | const store = writable(initialValue); 6 | store.subscribe((v) => { 7 | _value = v; 8 | }); 9 | return [store, () => _value] as const; 10 | } 11 | 12 | export const localStorageWritable = (key: string, initialValue: T) => { 13 | let stored = localStorage.getItem(key); 14 | const store = writable(stored ? JSON.parse(stored) : initialValue); 15 | store.subscribe((value) => localStorage.setItem(key, JSON.stringify(value))); 16 | return store; 17 | }; 18 | 19 | export const createLocalStorageWritable = (key: string, initialValue: T) => { 20 | let _value = initialValue; 21 | const store = localStorageWritable(key, initialValue); 22 | store.subscribe((v) => { 23 | _value = v; 24 | }); 25 | return [store, () => _value] as const; 26 | }; 27 | 28 | export type Cache = Record< 29 | S, 30 | { createdAt: number; value: U } 31 | >; 32 | 33 | export const createLocalStorageCache = ( 34 | key: string, 35 | fetcher: (key: K) => Promise, 36 | invalidateMilliseconds = 1000 * 60 * 60 * 24 37 | ) => { 38 | const initialValue = {} as Cache; 39 | const [cache, getCache] = createLocalStorageWritable(key, initialValue); 40 | 41 | const getter = async (key: K): Promise => { 42 | const now = new Date().getTime(); 43 | const cached = getCache()[key]; 44 | if (cached && now < cached.createdAt + invalidateMilliseconds) { 45 | return cached.value; 46 | } 47 | const value = await fetcher(key); 48 | cache.update((v) => { 49 | v[key] = { value: value, createdAt: now }; 50 | return v; 51 | }); 52 | return value; 53 | }; 54 | return getter; 55 | }; 56 | 57 | export const convertSpecialCharacters = (str: string) => { 58 | const tempElement = document.createElement("textarea"); 59 | tempElement.innerHTML = str; 60 | const val = tempElement.value; 61 | tempElement.remove(); 62 | return val; 63 | }; 64 | 65 | export const isNotNullOrUndefined = ( 66 | src: T | null | undefined 67 | ): src is T => { 68 | return src !== null && src !== undefined; 69 | }; 70 | 71 | export const rand = (max = 100000) => Math.floor(Math.random() * max); 72 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "virtual:uno.css"; 2 | import "@unocss/reset/tailwind-compat.css"; 3 | import "./index.scss"; 4 | import "tippy.js/dist/tippy.css"; 5 | import "simplebar/dist/simplebar.css"; 6 | import "easymde/dist/easymde.min.css"; 7 | import "./toast.scss"; 8 | import App from "./App.svelte"; 9 | 10 | const app = new App({ 11 | // @ts-expect-error 12 | target: document.getElementById("app"), 13 | }); 14 | 15 | export default app; 16 | -------------------------------------------------------------------------------- /src/router/route.ts: -------------------------------------------------------------------------------- 1 | import Home from "@/views/Home.svelte"; 2 | import Memo from "@/views/Memo.svelte"; 3 | import Work from "@/views/Work.svelte"; 4 | 5 | export const routes = { 6 | "/": Home, 7 | "/works/:id": Work, 8 | "/memos/:id": Memo, 9 | // TODO: 404 10 | }; 11 | -------------------------------------------------------------------------------- /src/store/memo.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const memo = writable< 4 | { workId: number; value: string; lastModified: "remote" | "local" }[] 5 | >([]); 6 | -------------------------------------------------------------------------------- /src/store/query.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | 3 | export const query = writable(""); 4 | -------------------------------------------------------------------------------- /src/store/seiya.ts: -------------------------------------------------------------------------------- 1 | import { commandGetNearestKeyAndDistance } from "@/lib/command"; 2 | import { getSeiyaDataPairs } from "@/lib/scrapeSeiya"; 3 | import type { SeiyaDataPair } from "@/lib/types"; 4 | import { createLocalStorageCache } from "@/lib/utils"; 5 | 6 | const createSeiya = () => { 7 | const getter = createLocalStorageCache<"master", SeiyaDataPair[]>( 8 | "seiya-cache", 9 | getSeiyaDataPairs 10 | ); 11 | 12 | const getUrl = async (gamename: string) => { 13 | const cache = await getter("master"); 14 | const [url, distance] = await commandGetNearestKeyAndDistance( 15 | gamename, 16 | cache 17 | ); 18 | return url; 19 | }; 20 | 21 | return { 22 | getUrl, 23 | }; 24 | }; 25 | 26 | export const seiya = createSeiya(); 27 | -------------------------------------------------------------------------------- /src/store/showSidebar.ts: -------------------------------------------------------------------------------- 1 | import { localStorageWritable } from "@/lib/utils"; 2 | 3 | export const showSidebar = localStorageWritable("show-sidebar", true); 4 | -------------------------------------------------------------------------------- /src/store/sidebarCollectionElements.ts: -------------------------------------------------------------------------------- 1 | import { commandGetAllElements } from "@/lib/command"; 2 | import type { 3 | CollectionElement, 4 | CollectionElementsWithLabel, 5 | } from "@/lib/types"; 6 | import { createWritable } from "@/lib/utils"; 7 | 8 | function createSidebarCollectionElements() { 9 | const [{ subscribe, update, set }, value] = createWritable< 10 | CollectionElement[] 11 | >([]); 12 | 13 | const refetch = async () => { 14 | set(await commandGetAllElements()); 15 | }; 16 | const updateLike = (id: number, isLike: boolean) => { 17 | const now = new Date(); 18 | const likeAt = `${now.getFullYear()}-${ 19 | now.getMonth() + 1 20 | }-${now.getDate()}`; 21 | update((elements) => 22 | elements.map((v) => 23 | v.id === id ? { ...v, likeAt: isLike ? likeAt : null } : { ...v } 24 | ) 25 | ); 26 | }; 27 | 28 | const [shown] = createWritable([]); 29 | 30 | return { 31 | subscribe, 32 | value, 33 | refetch, 34 | updateLike, 35 | shown, 36 | }; 37 | } 38 | 39 | export const sidebarCollectionElements = createSidebarCollectionElements(); 40 | -------------------------------------------------------------------------------- /src/store/startProcessMap.ts: -------------------------------------------------------------------------------- 1 | import { createWritable } from "@/lib/utils"; 2 | 3 | export const [startProcessMap, getStartProcessMap] = createWritable<{ 4 | [key: string]: number; 5 | }>({}); 6 | -------------------------------------------------------------------------------- /src/store/works.ts: -------------------------------------------------------------------------------- 1 | import { getWorkByScrape } from "@/lib/scrapeWork"; 2 | import type { Work } from "@/lib/types"; 3 | import { createLocalStorageCache } from "@/lib/utils"; 4 | 5 | const createWorks = () => { 6 | const getter = createLocalStorageCache( 7 | "works-cache", 8 | getWorkByScrape 9 | ); 10 | 11 | return { 12 | get: getter, 13 | }; 14 | }; 15 | 16 | export const works = createWorks(); 17 | -------------------------------------------------------------------------------- /src/toast.scss: -------------------------------------------------------------------------------- 1 | .toastify { 2 | padding: 12px 20px; 3 | color: #adbac7; 4 | display: block; 5 | box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12); 6 | background-color: #22272e; 7 | position: fixed; 8 | opacity: 0; 9 | transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); 10 | border-radius: 0.5rem; 11 | cursor: pointer; 12 | text-decoration: none; 13 | max-width: calc(50% - 20px); 14 | z-index: 100; 15 | } 16 | 17 | .toastify.on { 18 | opacity: 1; 19 | } 20 | 21 | .toast-close { 22 | background: transparent; 23 | border: 0; 24 | color: #adbac7; 25 | cursor: pointer; 26 | font-family: inherit; 27 | font-size: 1em; 28 | opacity: 0.4; 29 | padding: 0 5px; 30 | } 31 | 32 | .toastify-right { 33 | right: 15px; 34 | } 35 | 36 | .toastify-left { 37 | left: 15px; 38 | } 39 | 40 | .toastify-top { 41 | top: -150px; 42 | } 43 | 44 | .toastify-bottom { 45 | bottom: -150px; 46 | } 47 | 48 | .toastify-rounded { 49 | border-radius: 25px; 50 | } 51 | 52 | .toastify-avatar { 53 | width: 1.5em; 54 | height: 1.5em; 55 | margin: -7px 5px; 56 | border-radius: 2px; 57 | } 58 | 59 | .toastify-center { 60 | margin-left: auto; 61 | margin-right: auto; 62 | left: 0; 63 | right: 0; 64 | max-width: fit-content; 65 | max-width: -moz-fit-content; 66 | } 67 | 68 | @media only screen and (max-width: 360px) { 69 | .toastify-right, .toastify-left { 70 | margin-left: auto; 71 | margin-right: auto; 72 | left: 0; 73 | right: 0; 74 | max-width: fit-content; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/views/Work.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#await workPromise then work} 11 |
12 | 13 |
14 | {/await} 15 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | "baseUrl": ".", 9 | /** 10 | * Typecheck JS in `.svelte` and `.js` files by default. 11 | * Disable checkJs if you'd like to use dynamic types in JS. 12 | * Note that setting allowJs false does not prevent the use 13 | * of JS in `.svelte` files. 14 | */ 15 | "allowJs": false, 16 | "checkJs": false, 17 | "isolatedModules": true, 18 | "paths": { 19 | "@/*": [ 20 | "./src/*" 21 | ], 22 | }, 23 | "strict": true 24 | }, 25 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "*.cjs", "unocss.config.ts"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@unocss/vite"; 2 | import presetWind from "@unocss/preset-wind"; 3 | import presetAttributify from "@unocss/preset-attributify"; 4 | import presetWebFonts from "@unocss/preset-web-fonts"; 5 | import presetIcons from "@unocss/preset-icons"; 6 | import transformerVariantGroup from "@unocss/transformer-variant-group"; 7 | import extractorSvelte from "@unocss/extractor-svelte"; 8 | 9 | export default defineConfig({ 10 | presets: [ 11 | presetAttributify(), 12 | presetWind(), 13 | presetIcons(), 14 | extractorSvelte(), 15 | presetWebFonts({ 16 | fonts: { 17 | sans: [ 18 | { 19 | name: "Noto Sans JP", 20 | weights: ["400", "500", "700"], 21 | }, 22 | ], 23 | logo: [ 24 | { 25 | name: "Space Mono", 26 | weights: ["400"], 27 | }, 28 | ], 29 | }, 30 | }), 31 | ], 32 | transformers: [transformerVariantGroup()], 33 | theme: { 34 | colors: { 35 | accent: { 36 | accent: "#487AF9", 37 | success: "#347d39", 38 | edit: "#116329", 39 | warning: "#c69026", 40 | error: "#EA4E60", 41 | }, 42 | bg: { 43 | primary: "#22272e", 44 | secondary: "#2d333b", 45 | tertiary: "#323942", 46 | disabled: "#181818", 47 | button: "#373e47", 48 | buttonHover: "#444c56", 49 | backdrop: "#1C2128", 50 | successDisabled: "rgba(35,134,54,0.6)", 51 | successHover: "#46954a", 52 | warning: "#37332a", 53 | }, 54 | ui: { tertiary: "#636e7b" }, 55 | border: { 56 | primary: "#444c56", 57 | button: "#CDD9E5", 58 | buttonHover: "#768390", 59 | warning: "#AE7C14", 60 | successDisabled: "rgba(35,134,54,0.6)", 61 | }, 62 | text: { 63 | primary: "#adbac7", 64 | secondary: "#CDD9E5", 65 | tertiary: "#768390", 66 | link: "#2e7cd5", 67 | white: "#FFFFFF", 68 | disabled: "#484f58", 69 | successDisabled: "rgba(255,255,255,0.5)", 70 | }, 71 | }, 72 | fontSize: { 73 | body: ["1rem", "160%"], 74 | body2: [".875rem", "160%"], 75 | body3: [".8rem", "160%"], 76 | h1: ["1.75rem", "145%"], 77 | h2: ["1.5rem", "145%"], 78 | h3: ["1.25rem", "145%"], 79 | h4: ["1.125rem", "145%"], 80 | caption: [".75rem", "142%"], 81 | input: [".875rem", "100%"], 82 | }, 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /update.ps1: -------------------------------------------------------------------------------- 1 | # タグ名を引数から取得 2 | $tagName = $args[0] 3 | 4 | # Extract version (vを取り除く) 5 | $version = $tagName -replace '^v', '' 6 | 7 | # Update package.version in tauri.conf.json 8 | $tauriConfPath = "src-tauri\tauri.conf.json" 9 | $tauriConf = Get-Content -Path $tauriConfPath | ConvertFrom-Json 10 | $tauriConf.package.version = $version 11 | $tauriConf | ConvertTo-Json -Depth 100 | Set-Content -Path $tauriConfPath 12 | 13 | # Set Tauri Environment Variables 14 | $env:TAURI_PRIVATE_KEY = Get-Content -Path "~\.tauri\launcherg-actions.key" 15 | $env:TAURI_KEY_PASSWORD = Get-Content -Path "~\.tauri\launcherg-actions-pass.key" 16 | 17 | # Install dependencies and build 18 | Invoke-Expression "npm i" 19 | Invoke-Expression "npm run tauri build" 20 | 21 | # Update .tauri-updater.json 22 | $env:TEMP_SIGNATURE = Get-Content -Path ".\src-tauri\target\release\bundle\msi\Launcherg_${version}_x64_ja-JP.msi.zip.sig" 23 | $updaterData = @{ 24 | version = $version 25 | notes = "See the assets to download this version and install." 26 | pub_date = (Get-Date -Format s) + "Z" 27 | signature = $env:TEMP_SIGNATURE 28 | url = "https://github.com/ryoha000/launcherg/releases/download/${tagName}/Launcherg_${version}_x64_ja-JP.msi.zip" 29 | } 30 | $updaterData | ConvertTo-Json | Set-Content -Path ".tauri-updater.json" 31 | 32 | # Format files (Prettier のCLIを使ってファイルをフォーマット) 33 | Invoke-Expression "npx -y prettier $tauriConfPath .tauri-updater.json --write" 34 | 35 | # Push updated files to main 36 | git add $tauriConfPath .tauri-updater.json 37 | git commit -m "Update for release $version" 38 | git push origin main 39 | 40 | git tag $tagName 41 | git push origin $tagName 42 | 43 | # orphan なブランチを作って、そこにビルド結果をコミットする 44 | $artifactBranchName = "${tagName}-artifacts" 45 | git checkout --orphan $artifactBranchName 46 | git rm -rf . 47 | git add -f src-tauri/target/release/bundle/msi/Launcherg_${version}_x64_ja-JP.msi.zip 48 | git commit -m "Release $version" 49 | git push origin $artifactBranchName --force 50 | git switch main 51 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | import sveltePreprocess from "svelte-preprocess"; 4 | import UnoCSS from "unocss/vite"; 5 | import extractorSvelte from "@unocss/extractor-svelte"; 6 | import { fileURLToPath } from "node:url"; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(async () => ({ 10 | plugins: [ 11 | UnoCSS({ 12 | extractors: [extractorSvelte()], 13 | /* more options */ 14 | }), 15 | svelte({ 16 | preprocess: [ 17 | sveltePreprocess({ 18 | typescript: true, 19 | }), 20 | ], 21 | }), 22 | ], 23 | 24 | resolve: { 25 | preserveSymlinks: true, 26 | alias: { 27 | "@": fileURLToPath(new URL("./src", import.meta.url)), 28 | }, 29 | }, 30 | 31 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 32 | // prevent vite from obscuring rust errors 33 | clearScreen: false, 34 | // tauri expects a fixed port, fail if that port is not available 35 | server: { 36 | port: 1420, 37 | strictPort: true, 38 | }, 39 | // to make use of `TAURI_DEBUG` and other env variables 40 | // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand 41 | envPrefix: ["VITE_", "TAURI_"], 42 | build: { 43 | // Tauri supports es2021 44 | target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13", 45 | // don't minify for debug builds 46 | minify: !process.env.TAURI_DEBUG ? "esbuild" : false, 47 | // produce sourcemaps for debug builds 48 | sourcemap: !!process.env.TAURI_DEBUG, 49 | }, 50 | })); 51 | --------------------------------------------------------------------------------