DavidoTek / ProtonUp-Qt

Install and manage GE-Proton, Luxtorpeda & more for Steam and Wine-GE & more for Lutris with this graphical user interface.
https://davidotek.github.io/protonup-qt
GNU General Public License v3.0
1.18k stars 39 forks source link

Allow "subscribing" to sources for auto updates #117

Open brendenhoffman opened 1 year ago

brendenhoffman commented 1 year ago

Is your feature request related to a problem? Please describe. No, aside from updating is manual

Describe the solution you'd like This might be asking for a lot, but I have given it some thought to maybe give you a simple solution for auto updates. So, the premise is, the program would work as-is for explicitly installed apps, but auto updated apps will come from a subscribed list. ProtonUp-Qt can simply run in the background and poll updates, or the user can open the app to check for updates on the subscribed list. When a new version for an app is available ProtonUp-Qt will install it basically as if the user did so, and runs the batch update immediately after. The previous version can optionally be kept if the new version breaks anything, the rest will be auto deleted during the update process. Old unused versions can simply be uninstalled, if it is in use the user would have had to change that themselves, so it should not be uninstalled. The use state and version number should be a pretty reliable way to check what should be uninstalled. The only thing that I could see throwing a wrench in things is if an app changes the versioning scheme, but depending on how you have ProtonUp-Qt check if something is a new version or not, that may be an issue even without an auto updater, or it could be a non issue if you have already planned for that. I think this is a pretty user friendly solution and might just amount to a pretty small script. If I were a more experienced programmer I would give it a shot.

Describe alternatives you've considered Apps could be updated in place of the old one, but I think that would be even harder to implement and it would not have the flexibility of the batch update system you already have in place, plus there is less worries if an update breaks something. I imagine you have already given thought to updates and all of the reasons you wouldn't want that.

sonic2kk commented 1 year ago

Wrote a small Python script that gets the latest GitHub release tag for a project, it's possible that this could be used to get the latest available version on GitHub and compare it with what we have, and then ProtonUp-Qt could prompt and ask for an update

import requests
import re
import os

def get_latest_github_tag(project_url):

    github_url = 'https://github.com'

    releases_url = f'{project_url}/releases'.replace(github_url, '')
    tags_url = f'{project_url}/tags'

    tagspage_resp = requests.get(tags_url).text
    release_tags = re.search(f'{releases_url}[^\"]+', tagspage_resp)

    return os.path.basename(release_tags.group(0))

ge_tag = get_latest_github_tag('https://github.com/GloriousEggroll/proton-ge-custom')
stl_tag = get_latest_github_tag('https://github.com/sonic2kk/steamtinkerlaunch')
pupqt_tag = get_latest_github_tag('https://github.com/DavidoTek/ProtonUp-Qt')
boxtron_tag = get_latest_github_tag('https://github.com/dreamer/boxtron')
luxtorpeda_tag = get_latest_github_tag('https://github.com/luxtorpeda-dev/luxtorpeda')
kron4ek_tag = get_latest_github_tag('https://github.com/Kron4ek/Wine-Builds')

# Seems like TKG doesn't return anything useful...
#protontkg_tag = get_latest_github_tag('https://github.com/Frogging-Family/wine-tkg-git')

print(f'Latest GE-Proton release is: {ge_tag}')
print(f'Latest SteamTinkerLaunch release is: {stl_tag}')
print(f'Latest ProtonUp-Qt release is: {pupqt_tag}')
print(f'Latest Boxtron release is: {boxtron_tag}')
print(f'Latest Luxtorpeda release is: {luxtorpeda_tag}')
print(f'Latest Kron4ek Wine release is: {kron4ek_tag}')
This was based on something similar I did for STL in Bash, to fetch the latest version of MO2 and Vortex ```bash #!/usr/bin/env bash # I adapted the STL code into the below code to only get the latest tag, and then wrote the Python version above GHURL="https://github.com" WGET="wget" function getLatestGitHubReleaseVer { PROJURL="$1" RELEASESURL="${PROJURL}/releases" EXPANDEDASSETSURL="${RELEASESURL}/expanded_assets" TAGSURL="${PROJURL}/tags" TAGSGREP="${RELEASESURL#"$GHURL"}/tag" LATESTTAG="$("$WGET" -q "${TAGSURL}" -O - 2> >(grep -v "SSL_INIT") | grep -m1 "$TAGSGREP" | grep -oE "${TAGSGREP}[^\"]+")" LATESTVER="${LATESTTAG##*/}" echo "$LATESTVER" } echo "Latest GE ver: $( getLatestGitHubReleaseVer "https://github.com/GloriousEggroll/proton-ge-custom" )" echo "Latest STL ver: $( getLatestGitHubReleaseVer "https://github.com/sonic2kk/steamtinkerlaunch" )" echo "Latest ProtonUp-Qt ver: $( getLatestGitHubReleaseVer "https://github.com/DavidoTek/ProtonUp-Qt" )" ```

Just in case anyone was thinking about this :-)

DavidoTek commented 1 year ago

We could include it in the ctmod and run it somehow on every startup for each detected compatiblity tool. Then show a message box or indicate it with a :arrow_up: (arrow up) beside the compatibility tool.

sonic2kk commented 1 year ago

Ctmods could have an update_tool method and maybe an update_available property? Then we can show the up arrow on the compat tool list and a user can double click / press an "Update" button (which would be greyed out / invisible if no update is available - not sure which is good UX) it can show a dialog with the update information.

The dialog would have some information like current version and the version being updated, the path, number of games using the tool, and a button to update, cancel and view the release notes (if applicable -- i.e. take a user to the latest GitHub release page for the latest GE-Proton). When they click "Update", it would call an an available update method on a ctmod, and then batch update tools that are using that compat tool to the latest version.


Ctmods would be responsible for implementing a check_update_available method too, and we shouldn't show the update icon on the ctmod list unless a ctmod has this method. This could be responsible for setting the update_available property.

Since the upgrade path would be different for each tool (DXVK vs GE-Proton vs SteamTinkerLaunch etc etc), having this as a method would be a good idea. And we shouldn't show an "update available" unless a tool has this method also.

Basically, if a tool hasn't implemented the proper methods first, we shouldn't even try checking if it has updates.


The consideration here is that, one of the benefits of a tool like ProtonUp-Qt is being able to manage several versions of things like GE-Proton. Showing an update available on all compat tools might be a bit... visually noisy? Imagine having 5 different releases of GE-Proton, but there is only one that you really want as your "main" version, and the others are for specific games. Seeing an :arrow_up: beside all of them could be visually noisy. That's just a minor "UX" consideration though :-)

Another consideration here is that, a user might have GE-Proton7-39, GE-Proton7-40 and GE-Proton7-41 at the moment. In this case, 7-39 and 7-40 would show updates being available. However this would overwrite the existing GE-Proton7-41. I am not sure what the best behaviour would be here, it's just something I realised today while thinking about how a feature like this would work. What is the best way to handle updating tools to a version that is already downloaded? There could be the route of showing updates for all compat tools, but then also the route of only showing an update for the latest available version of a tool.


None of this is meant to put this feature request down either, I actually think this would be a really neat feature. There are just various things to consider, and documenting / discussing them here could help turn this issue into a better reference for implementation.

DavidoTek commented 1 year ago

Then we can show the up arrow on the compat tool list and a user can double click / press an "Update" button

Yes

Ctmods would be responsible for implementing a check_update_available method too

Each ctmod could have a method get_newest_version responsible for fetching the newest version number for the tool.

Then we need some way to compare the newest version against the newest installed version. First idea: Ideally local compatibility tools would also not be loaded by a global ls <dir>-like function (currently get_installed_ctools in util.py), but rather each ctmod would have a method get_installed_versions that would return a CompatTool object. That would also simplify removing the tools. Each ctmod would then have a child class of CompatTool that implements functions like get_install_dir, get_tool_launcher and remove_tool

Showing an update available on all compat tools might be a bit... visually noisy?

Grouping the tools would be great. Not sure how good/bad that would look. Maybe like this (quite complex to realitze I guess and would assume we implemented a get_installed_version for each compatibility tool):

> Boxtron                                              multiple version of Boxtron, collapsed

ᐯ GE-Proton                       [🡹]                 multiple version of GE-Proton, expanded, update available
     v16.9
     v17.0-2

  Luxtorpedia v40                 [🡹] v41             single version of Luxtorpeda, update available
sonic2kk commented 1 year ago

I meant to reply to this and forgot...


I'm not sure of the best way to "create" it with Qt, but I think this is a good idea - Basically a parent tool "category" with a dropdown for each version of the tool. Maybe by default, all the tools should be expanded?

I am sure Qt has an element for this, but getting the UI right on it so that it isn't too much of a "striking" change while keeping it friendly is something to consider.

DavidoTek commented 1 year ago

Sounds like a use case for a tree view. It's a bit more complex than a list view as it uses the QAbstractItemModel, but that makes it also more flexible.

We would need to find a way to group the compatibility tools which is the hardest part I think. We could use a regular expression (or just 'string'.contains()) to check if an installed to belongs to a ctmod.

I would like to add a function like this to each ctmod (I sketched a concept to make ProtonUp-Qt more modular a while ago but hadn't time yet to implement it):

class CtInstallerDxvk(CtModSuperClass):
    ...
    def get_installed_versions(self, launcher: Launcher) -> List[CompatTool]:
        tools = []
        install_dir = os.path.join( launcher.get_base_dir(), 'runtime/dxvk' )
        for f in os.listdir(install_dir):
            if not _some_regex_magic(f):
                continue
            version = _some_regex_magic(f)
            tools.append( CompatTool(name='DXVK', launcher=launcher, version=version,
                                       absolute_install_dir=install_dir, ctinstaller=CtInstallerDxvk) )
        return tools

    def get_newest_release(self) -> CompatTool:
        newest_version = _fetch_newest_version_from_github()
        return CompatTool(name='DXVK', version=newest_version, installed=False, ctinstaller=CtInstallerDxvk)

pupgui2.api.register_ctinstaller(CtInstallerDxvk, launchers=['lutris'], packages=['native', 'flatpak'])
sonic2kk commented 1 year ago

Grouping is definitely going to be the tricky part, which I ran into when creating this (extremely rough) mockup:

image

This is really just a UI proof-of-concept, the grouping is massively hardcoded and also since it uses a TreeView, the signals aren't working (they would need to be set up differently, since a double-click on the top level item has different behaviour). So right now it isn't very useful other than "visualizing"

SteamTinkerLaunch and Luxtorpeda are also in subcategories -- Probably there would need to be something maybe in the ctmod where it's set whether this can be "grouped" or not because right now it may have to be hardcoded / set in a dictionary or something.


The code to populate the list view is like this (I have a branch too just for fun):

def update_ui(self):
    # ...
    self.populate_treeview_with_ctools(['GE-Proton', 'Proton-Tkg', 'SteamTinkerLaunch', 'Luxtorpeda'])
    # ...

def populate_treeview_with_ctools(self, ctool_names):
    ctool_toplevels = {}
    for ct in self.compat_tool_index_map:
        ctool_name = ct.get_displayname(unused_tr=self.tr('unused'))
        ctool_widget = QTreeWidgetItem([ctool_name])
        for cn in ctool_names:
            if cn.lower() in ctool_name.lower():
                if cn not in ctool_toplevels.keys():
                    ctool_toplevel_widget = QTreeWidgetItem([cn])
                    self.ui.listInstalledVersions.addTopLevelItem(ctool_toplevel_widget)
                    ctool_toplevels[cn] = ctool_toplevel_widget

                ctool_toplevels[cn].addChild(ctool_widget)

Of course none of this is even close to any sort of even functional implementation, and at least in its current iteration it's not something I'd dream of putting a PR up against. This is just to show off what I came up with on a visual level -- Under the hood there could be many improvements made across the board to better facilitate this (even just in this example, having the compat tools for Steam/Lutris/etc as lists in constants.py would be an improvement off the top of my head).

Maybe the ideas here could serve as a basis though. Making ProtonUp-Qt more modular would be pretty sweet and help a lot here (then various properties could just be taken from the ctool in a loop to generate the tree view). Implementing something like this should probably wait until there are other changes to better allow for integrating this sort of feature.

DavidoTek commented 1 year ago

That looks great! Yes, we would need to change that quite a bit and I think the compat_tool_index_map` wouldn't be sufficient anymore.

I also started working on adding a get_installed_versions method to the ctmods, but there is still a lot to do.