├── .gitignore ├── LICENSE ├── README.md ├── bin └── reload.sh ├── components ├── Alert.js ├── AppExport │ ├── AdvancedConfig.js │ ├── ExportApps.js │ ├── GenericExport.js │ └── InputComponents.js ├── AppIcon.js ├── CreatePackForm.js ├── DonateCard.js ├── Error.js ├── FeaturePromoter.js ├── Footer.js ├── ListPackages.js ├── ListSort.js ├── MetaTags.js ├── Nav.js ├── PackAppsList.js ├── PackPreview.js ├── PageWrapper.js ├── PopularApps.js ├── PrettyApp.js ├── RecentApps.js ├── Recommendations.js ├── Search.js ├── SelectionBar.js └── SingleApp.js ├── ctx ├── PopularContext.js └── SelectedContext.js ├── data └── popularApps.json ├── next.config.js ├── package.json ├── pages ├── 404.js ├── _app.js ├── _document.js ├── api │ ├── auth │ │ └── [...nextauth].js │ └── twitter.js ├── apps.js ├── apps │ └── [id].js ├── eli5.js ├── generate.js ├── index.js ├── packs │ ├── [id].js │ ├── create.js │ ├── edit.js │ └── index.js ├── privacy.js ├── sitemap.xml.js └── users │ ├── [id].js │ └── you.js ├── public ├── assets │ ├── apps │ │ ├── chrome.webp │ │ ├── code-insiders.webp │ │ ├── code.webp │ │ ├── discord.webp │ │ ├── dropbox.webp │ │ ├── edge.webp │ │ ├── fallback │ │ │ ├── chrome.png │ │ │ ├── code-insiders.png │ │ │ ├── code.png │ │ │ ├── discord.png │ │ │ ├── dropbox.png │ │ │ ├── edge.png │ │ │ ├── firefox.png │ │ │ ├── node.png │ │ │ ├── notion.png │ │ │ ├── npp.png │ │ │ ├── obs.png │ │ │ ├── python.png │ │ │ ├── sharex.png │ │ │ ├── spotify.png │ │ │ ├── teams.png │ │ │ ├── terminal.png │ │ │ ├── trumpet.png │ │ │ ├── tweeten.png │ │ │ ├── whatsapp.png │ │ │ └── zoom.png │ │ ├── firefox.webp │ │ ├── node.webp │ │ ├── notion.webp │ │ ├── npp.webp │ │ ├── obs.webp │ │ ├── python.webp │ │ ├── sharex.webp │ │ ├── spotify.webp │ │ ├── teams.webp │ │ ├── terminal.webp │ │ ├── trumpet.webp │ │ ├── tweeten.webp │ │ ├── whatsapp.webp │ │ └── zoom.webp │ ├── clippy.png │ ├── dl.svg │ ├── hero.png │ ├── logo.svg │ ├── packsPromo.svg │ └── winget-pro.svg ├── cover.png ├── favicon.ico ├── generic-app-icon.svg ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt ├── vercel-dark.svg └── vercel.svg ├── styles ├── alert.module.scss ├── appList.module.scss ├── apps.module.scss ├── base.scss ├── create.module.scss ├── donateCard.module.scss ├── error.module.scss ├── exportApps.module.scss ├── featurePromoter.module.scss ├── footer.module.scss ├── home.module.scss ├── listSort.module.scss ├── nav.module.scss ├── packPage.module.scss ├── packPreview.module.scss ├── packsAppList.module.scss ├── prettyApp.module.scss ├── recommendations.module.scss ├── search.module.scss └── singleApp.module.scss ├── utils ├── fetchWinstallAPI.js ├── generateWingetImport.js └── helpers.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .next 25 | diff.js 26 | 27 | .env 28 | 29 | **/public/workbox-*.js 30 | **/public/sw.js 31 | **/public/workbox-*.js.map 32 | **/public/sw.js.map 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # winstall 2 | 3 | ## A web app for browsing Windows Package Manager apps, and create a batch-installation command using an intuitive interface. 4 | 5 | How does it work? 6 | ----------------------- 7 | 8 | winstall is powered by Windows Package Manager (aka "winget"), [Microsoft's new package manager](https://devblogs.microsoft.com/commandline/windows-package-manager-preview/) for Windows 10 and 11. it is available by default in Windows 10 and 11. 9 | 10 | Installing Windows Package Manager 11 | ---------------------------------- 12 | 13 | If you don't already have Windows Package Manager, you can install it by downloading and installing the [latest .appxbundle file from here.](https://github.com/microsoft/winget-cli/releases) 14 | 15 | Using winstall 16 | -------------- 17 | 18 | To use winstall, you can search for apps on the homepage. Additionally, you can also view all the apps available via Windows Package Manager [on this page.](https://winstall.app//apps) 19 | 20 | Simply select the apps you want to download and click on the "Generate Script" button at the bottom of the screen. You will then be presented with a command that you can copy and paste into any Windows command-line. Input that into a command line app of your choice, and hit enter to start installing the apps one-by-one using Windows Package Manager. You can also generate a PowerShell script by toggling the "Show Powershell scirpt" option. 21 | 22 | Alternatively, you can click on the "Download .bat/.ps1" button which will download a batch file. However, you will likely get a security warning from your browser. In that case, ignore the warning as the batch file is completely secure. Once downloaded, you can double-click the .bat/ file to install the apps using the Windows Package Manager. 23 | 24 | How is the data obtained? 25 | ------------------------- 26 | 27 | winstall is powered by an API that I have built. The API regularly checks Microsoft's [official repository](https://github.com/microsoft/winget-pkgs) for Windows Package Manager apps. This means it always provides the latest data. 28 | 29 | The API updates its data every 15 minutes on weekdays, and every 3 hours on weekends. I will be making the API open-source in the near-future. 30 | 31 | Popular apps 32 | ------------ 33 | 34 | The list of popular apps are fetched from a `.json` file with a pre-populated set of data. On the front-end, a random selection of 6 apps from the list is displayed. If you would like to add an app to the list of popular apps, you can do so by [adding an app here](https://github.com/omaha-consulting/winstall/blob/master/data/popularApps.json) and creating a pull request. You will also have to provide a logo for that app, which needs to have a transparent image, be 80x80px, and in the .webp format. The logo must be [added in this folder.](https://github.com/omaha-consulting/winstall/tree/master/public/assets/apps). And because Safari does not like .webp, you need to also add a .png version of the same image under /apps/fallback. 35 | 36 | History 37 | ------------ 38 | winstall was originally created by [Mehedi Hassan](https://github.com/MehediH) as a side project, but is now owned and maintained by [winget.Pro](https://winget.pro) - with [winget.Pro](https://winget.pro), you can have your own, securely hosted repositories for the Windows Package Manager. 39 | -------------------------------------------------------------------------------- /bin/reload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source ~/.bashrc 4 | 5 | cd /srv 6 | npm run build 7 | pm2 delete all 8 | pm2 start npm --name "next-app" -- start 9 | pm2 save -------------------------------------------------------------------------------- /components/Alert.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { FiX } from "react-icons/fi"; 3 | import styles from "../styles/alert.module.scss"; 4 | 5 | const Alert = ({ text, id, children, accent=false }) => { 6 | const [hidden, setHidden] = useState(true); 7 | 8 | useEffect(() => { 9 | const isHidden = localStorage.getItem(`alert-${id}`); 10 | 11 | if(!isHidden) setHidden(false); 12 | }) 13 | 14 | const handleHide = () => { 15 | localStorage.setItem(`alert-${id}`, true); 16 | setHidden(true); 17 | } 18 | 19 | if (hidden) return null; 20 | 21 | return ( 22 |
23 | {children} 24 |

{text}

25 | 26 |
27 | ) 28 | } 29 | 30 | export default Alert; -------------------------------------------------------------------------------- /components/AppExport/AdvancedConfig.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { FiChevronDown, FiInfo } from "react-icons/fi"; 3 | import styles from "../../styles/exportApps.module.scss"; 4 | import { CheckboxConfig, RadioConfig, TextInputConfig } from "./InputComponents"; 5 | 6 | const AdvancedConfig = ({ refreshConfig, activeTab }) => { 7 | const [ expanded, setExpnaded ] = useState(false); 8 | 9 | const [ config, setConfig ] = useState({ 10 | "--scope": null, 11 | "-i": false, 12 | "-h": false, 13 | "-o": "", 14 | "--override": false, 15 | "-l": "", 16 | "--force": false, 17 | "--ignore-unavailable": false 18 | }); 19 | 20 | const [ hiddenOptions, setHiddenOptions ] = useState([]); 21 | 22 | const updateConfig = async (key, val) => { 23 | const existingConfig = { ...config }; 24 | const newConfig = { ...existingConfig, [key]: val}; 25 | 26 | setConfig(newConfig); 27 | refreshConfig(newConfig, hiddenOptions); 28 | 29 | await localStorage.setItem("winstall-advanced-config", JSON.stringify(newConfig)); 30 | } 31 | 32 | useEffect(() => { 33 | const loadExistingConfig = async (unavailableOptions) => { 34 | let previousConfig = await localStorage.getItem("winstall-advanced-config") 35 | 36 | if(previousConfig){ 37 | previousConfig = JSON.parse(previousConfig); 38 | setExpnaded(true); 39 | } else{ 40 | previousConfig = { ...config }; 41 | } 42 | 43 | refreshConfig(previousConfig, unavailableOptions); 44 | setConfig(previousConfig); 45 | } 46 | 47 | 48 | let unavailableOptions = ["--ignore-unavailable"]; 49 | 50 | if(activeTab === ".json"){ 51 | unavailableOptions = ["--scope", "-i", "-h", "-o", "--override", "-l", "--force"]; 52 | } 53 | 54 | setHiddenOptions(unavailableOptions); 55 | loadExistingConfig(unavailableOptions); 56 | 57 | }, [ activeTab ]); 58 | 59 | 60 | return ( 61 |
62 |

setExpnaded(e => !e)}> 63 | 64 | Advanced Options 65 |

66 | 67 | { expanded && ( 68 |
69 |

All of the following options are persisted locally in your browser.

70 | 71 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 | )} 92 |
93 | ) 94 | } 95 | 96 | export default AdvancedConfig; -------------------------------------------------------------------------------- /components/AppExport/ExportApps.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from "react"; 2 | import styles from "../../styles/exportApps.module.scss"; 3 | import generateWingetImport from "../../utils/generateWingetImport"; 4 | import GenericExport from "./GenericExport"; 5 | import AdvancedConfig from "./AdvancedConfig"; 6 | 7 | const ExportApps = ({ apps, title, subtitle }) => { 8 | const [ batScript, setBatScript ] = useState(""); 9 | const [ psScript, setPsScript ] = useState(""); 10 | const [ wingetScript, setWingetScript ] = useState(""); 11 | const [ filters, setFilters ] = useState({}); 12 | const [ wingetImportCommand, setWingetImportCommand ] = useState(""); 13 | const [active, setActive] = useState(".bat"); 14 | 15 | const tabs = useMemo(() => { 16 | return [ 17 | { 18 | title: "Batch", 19 | key: ".bat", 20 | element: 21 | }, 22 | { 23 | title: "PowerShell", 24 | key: ".ps1", 25 | element: 26 | }, 27 | { 28 | title: "Winget Import", 29 | key: ".json", 30 | element: 37 | } 38 | ] 39 | }, [ batScript, psScript, wingetScript, wingetImportCommand, apps ]) 40 | 41 | 42 | let handleScriptChange = async () => { 43 | if(!apps) return; 44 | 45 | let installs = []; 46 | 47 | let advancedFilters = ""; 48 | 49 | if(filters){ 50 | advancedFilters = Object.entries({ ...filters} ).filter(i => i[1] === true).map(i => i[0]).join(" "); 51 | 52 | if(filters["-o"]) advancedFilters += ` -o "${filters["-o"]}"`; 53 | if(filters["-l"]) advancedFilters += ` -l "${filters["-l"]}"`; 54 | if(filters["--scope"]) advancedFilters += ` --scope "${filters["--scope"]}"`; 55 | } 56 | 57 | apps.map((app) => { 58 | installs.push( 59 | `winget install --id=${app._id}${app.selectedVersion !== app.latestVersion ? ` -v "${app.selectedVersion}"` : ""} -e ${advancedFilters}` 60 | ); 61 | 62 | return app; 63 | }); 64 | 65 | let newBatchScript = installs.join(" && "); 66 | let newPSScript = installs.join(" ; "); 67 | 68 | 69 | setBatScript(newBatchScript); 70 | setPsScript(newPSScript); 71 | setWingetScript(JSON.stringify(await generateWingetImport(apps), 2)); 72 | 73 | setWingetImportCommand(`winget import --import-file "$fileName" ${advancedFilters}`); 74 | 75 | }; 76 | 77 | useEffect(() => { 78 | const restoreDefaultTab = async () => { 79 | const defaultExportTab = await localStorage.getItem("winstall-default-export-tab"); 80 | 81 | if(defaultExportTab !== null){ 82 | setActive(defaultExportTab); 83 | } 84 | } 85 | 86 | restoreDefaultTab(); 87 | handleScriptChange(); 88 | }, [handleScriptChange]); 89 | 90 | const changeTab = async ( tabKey ) => { 91 | setActive(tabKey); 92 | await localStorage.setItem("winstall-default-export-tab", tabKey); 93 | } 94 | 95 | const refreshFilters = async ( newConfig, unavailableOptions ) => { 96 | let availableConfig = { ...newConfig } 97 | 98 | await unavailableOptions.map(opt => { 99 | delete availableConfig[opt] 100 | }); 101 | 102 | setFilters(availableConfig); 103 | } 104 | 105 | return ( 106 |
107 | 108 | { title && ( 109 |
110 |

{title}

111 |
112 | )} 113 | 114 | { subtitle &&

{subtitle}

} 115 | 116 |
    117 | { tabs.map((tab, index) => { 118 | return
  • changeTab(tab.key)}>{tab.title}
  • 119 | }) } 120 |
121 | 122 | 123 | 124 | { tabs.map((tab, index) => { 125 | return
{ tab.element }
126 | }) } 127 | 128 |
129 | ) 130 | } 131 | 132 | export default ExportApps; -------------------------------------------------------------------------------- /components/AppExport/GenericExport.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { FiCopy, FiDownload, FiInfo } from "react-icons/fi"; 3 | import styles from "../../styles/exportApps.module.scss"; 4 | 5 | let handleCopy = ( fileContent, setCopyText ) => { 6 | navigator.clipboard.writeText(fileContent).then(() => { 7 | if(setCopyText) setCopyText("Copied!") 8 | }).catch(() => { 9 | document.querySelector("textarea").select(); 10 | }) 11 | } 12 | 13 | let handleDownload = ( fileContent, fileExtension, downloadId ) => { 14 | let dl = document.querySelector("#gsc"); 15 | dl.setAttribute("download", `winstall-${downloadId}${fileExtension}`) 16 | dl.href = "data:text/plain;base64," + btoa(fileContent); 17 | dl.click(); 18 | } 19 | 20 | const GenericExport = ({ fileContent, displayedCommand, fileExtension, prioritiseDownload=false, tip }) => { 21 | const [copyText, setCopyText] = useState("Copy to clipboard"); 22 | const [textboxContent, setTextboxContent] = useState(); 23 | const [downloadId, setDownloadId] = useState(Math.floor(1000 + Math.random() * 9000)); 24 | 25 | useEffect(() => { 26 | setCopyText("Copy to clipboard"); 27 | 28 | setTextboxContent(displayedCommand ? displayedCommand.replace("$fileName", `winstall-${downloadId}.json`) : fileContent); 29 | }, [fileContent, displayedCommand, downloadId]) 30 | 31 | return ( 32 |
33 |