Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
13e8b45
implements splash screen asking user to wait until workfile has been …
Jan 13, 2026
759c860
adds database config checks
Jan 13, 2026
fde27ae
compares ayon workfile with database project and gives options to eit…
Jan 13, 2026
d6af08f
fixes ayon file save when starting an entirely new project
Jan 13, 2026
a1283b2
Merge branch 'develop' into contrib/workfile-comparison-and-better-pr…
tweak-wtf Feb 27, 2026
a5df833
only set database if the current isn't the correct one
Feb 27, 2026
ac08747
Merge branch 'develop' into contrib/workfile-comparison-and-better-pr…
Mar 31, 2026
6bfb14f
removes unused import
Mar 31, 2026
8e4e46e
checks for local db project existence
Mar 31, 2026
d84c777
removes unused list comprehension
Mar 31, 2026
81bb143
includes ip check even if Disk database is configured which doesn't i…
Mar 31, 2026
bf35006
raises on all platforms other than win32
Mar 31, 2026
c3d9adf
wraps splash screen process kill in try/except
Mar 31, 2026
46e90ef
runs splashscreen without creation flags on platforms other than windows
Mar 31, 2026
3642a79
pleases linter
Mar 31, 2026
49455c2
adds configured db ip to DatabaseMisconfigurationError
Apr 7, 2026
916420d
handles db_ip field if db_type is `Disk`, adds description to db_ip f…
Apr 7, 2026
4a2eeaf
fixes local db always being considered invalid if db_ip isn't set
Apr 7, 2026
eb3fd7f
adds initial project save
Apr 7, 2026
96a46fb
catch project import being none
Apr 7, 2026
e88c06f
lowers loglevel
Apr 7, 2026
1f090e4
adds option to reimport incremented workfile instead of renaming the …
Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions client/ayon_resolve/api/menu.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
from datetime import datetime as dt

from qtpy import QtWidgets, QtCore, QtGui

Expand Down Expand Up @@ -145,6 +146,84 @@ def on_set_resolution_clicked(self):
print("Clicked Set Resolution")


class DatabaseMisconfigurationWarning(QtWidgets.QMessageBox):
def __init__(self, requested_db, available_dbs, parent=None):
_app = QtWidgets.QApplication.instance()
if not _app:
_app = QtWidgets.QApplication([])
super(DatabaseMisconfigurationWarning, self).__init__(parent)

self.parent = parent
self.requested_db = requested_db
self.available_dbs = available_dbs

self.setup_ui()

def setup_ui(self):
stylesheet = load_stylesheet()
self.setStyleSheet(stylesheet)

self.setWindowTitle("Project Database Warning")
self.setIcon(QtWidgets.QMessageBox.Warning)
self.setStandardButtons(QtWidgets.QMessageBox.Ok)

self.setWindowFlags(
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
)

self.setText(
"The requested project database can't be found.\n"
"No workfile will be opened. Please revisit AYON project settings.\n"
"Proceed at risk of losing local work."
)
report = (
"Requested project database:\n"
f"\tDB Type: {self.requested_db['db_type']}\n"
f"\tDB Name: {self.requested_db['db_name']}\n"
f"\tDB IP: {self.requested_db.get('db_ip', '')}\n"
"Available project databases:\n"
)
for db in self.available_dbs:
report += f"\t- DB Type: {db['DbType']}\n"
report += f"\t DB Name: {db['DbName']}\n"
self.setDetailedText(report)


class ProjectImportChooser(QtWidgets.QMessageBox):
def __init__(self, datemod_drp, datemod_dbp, parent=None):
_app = QtWidgets.QApplication.instance()
if not _app:
_app = QtWidgets.QApplication([])
super().__init__(parent)

self.parent = parent
self.datemod_drp = datemod_drp
self.datemod_dbp = datemod_dbp

self.setup_ui()

def setup_ui(self):
stylesheet = load_stylesheet()
self.setStyleSheet(stylesheet)

self.setWindowTitle("Recent Export")
self.setIcon(QtWidgets.QMessageBox.Warning)
self.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)

self.setText(
"A more recent project version was found exported.\n"
"Do you want to re-import the DRP?\n"
"Cancel for opening the existing database project\n"
"Ok for re-importing"
)
dt_drp = dt.fromtimestamp(self.datemod_drp).strftime('%Y-%m-%d %H:%M:%S')
dt_dbp = dt.fromtimestamp(self.datemod_dbp).strftime('%Y-%m-%d %H:%M:%S')
self.setDetailedText(
f"Date Modified DRP: {dt_drp}\n"
f"Date Modified DB: {dt_dbp}\n"
)


def launch_ayon_menu():
app = (
QtWidgets.QApplication.instance()
Expand Down
63 changes: 63 additions & 0 deletions client/ayon_resolve/api/splash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import sys
from pathlib import Path
from qtpy import QtWidgets, QtCore, QtGui

from ayon_core.style import load_stylesheet


class SplashWidget(QtWidgets.QWidget):
def __init__(self):
super(SplashWidget, self).__init__()
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint)

layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)

lbl_spinner = QtWidgets.QLabel()
gif_path = Path(__file__).parent / "ayon_spinner_white_pong_360.gif"
movie = QtGui.QMovie(gif_path.as_posix())
movie.setScaledSize(QtCore.QSize(64, 64))
lbl_spinner.setMovie(movie)
movie.start()
layout.addWidget(lbl_spinner)

lbl_info = QtWidgets.QLabel("Please wait...\nYour workfile is being opened.")
font = lbl_info.font()
font.setPointSize(16)
lbl_info.setFont(font)
layout.addWidget(lbl_info)

self.center_on_screen()

def center_on_screen(self):
if hasattr(QtWidgets.QApplication, "desktop"):
# Qt5 / PySide2
desktop = QtWidgets.QApplication.desktop()
screen_rect = desktop.screenGeometry(desktop.primaryScreen())
else:
# Qt6 / PySide6
screen = QtWidgets.QApplication.primaryScreen()
screen_rect = screen.geometry()

self.adjustSize()
window_rect = self.geometry()
self.move(
int(screen_rect.center().x() - window_rect.width() / 2),
int(screen_rect.center().y() - window_rect.height() / 2)
)


def main():
app = QtWidgets.QApplication.instance()
if not app:
app = QtWidgets.QApplication([])

app.setStyleSheet(load_stylesheet())

widget = SplashWidget()
widget.show()
sys.exit(app.exec_())


if __name__ == "__main__":
main()
190 changes: 176 additions & 14 deletions client/ayon_resolve/api/workio.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
"""Host API required Work Files tool"""

import os
import sys
import time
import hashlib
from pathlib import Path

from qtpy import QtWidgets

from ayon_core.lib import Logger
from ayon_core.settings import get_project_settings
from ayon_core.pipeline.context_tools import get_current_project_name

from .lib import (
get_project_manager,
get_current_resolve_project
get_current_resolve_project,
set_project_manager_to_folder_name
)
from .menu import DatabaseMisconfigurationWarning, ProjectImportChooser


log = Logger.get_logger(__name__)
Expand All @@ -23,22 +35,54 @@ def has_unsaved_changes():

def save_file(filepath):
project_manager = get_project_manager()
file = os.path.basename(filepath)
fname, _ = os.path.splitext(file)
project_saved = project_manager.SaveProject()
if not project_saved:
log.error("Failed to save current project!")
return False

resolve_project = get_current_resolve_project()
name = resolve_project.GetName()
incoming_wf = Path(filepath)
current_wf = incoming_wf.with_name(
resolve_project.GetName() + ".drp")

response = False
if name == "Untitled Project":
response = project_manager.CreateProject(fname)
log.info("New project created: {}".format(response))
project_manager.SaveProject()
elif name != fname:
response = resolve_project.SetName(fname)
log.info("Project renamed: {}".format(response))
# handle project db override if set
project_name = get_current_project_name()
settings = get_project_settings(project_name)
override_is_valid = True
if settings["resolve"]["project_db"].get("enabled", False):
log.info("Handling project database override...")
overrides = settings["resolve"]["project_db"]
override_is_valid = handle_project_db_override(
project_name, overrides
)
if not override_is_valid:
return False

exported = project_manager.ExportProject(fname, filepath)
log.info("Project exported: {}".format(exported))
rename_db_project = settings["resolve"].get("rename_db_project_on_increment", True)
if "Untitled Project" in current_wf.stem:
# saving initial workfile from currently opened project
project_manager.CreateProject(incoming_wf.stem)
project_manager.SaveProject()
exported = project_manager.ExportProject(incoming_wf.stem, incoming_wf.as_posix())
log.info(f"New project {incoming_wf.stem} exported: {exported}")
if current_wf.stem != incoming_wf.stem:
# workfile shall be incremented
if rename_db_project:
# increment with local renaming
resolve_project.SetName(incoming_wf.stem)
exported = project_manager.ExportProject(incoming_wf.stem, incoming_wf.as_posix())
log.info(f"Incremented workfile with local rename to {incoming_wf.as_posix()}: {exported}")
else:
# increment without local renaming but reimport
exported = project_manager.ExportProject(current_wf.stem, current_wf.as_posix())
exported = project_manager.ExportProject(current_wf.stem, incoming_wf.as_posix())
project_manager.ImportProject(incoming_wf.as_posix())
project_manager.LoadProject(incoming_wf.stem)
log.info(f"Incremented workfile with reimport to {incoming_wf.as_posix()}: {exported}")
else:
# workfile export without increment
exported = project_manager.ExportProject(incoming_wf.stem, incoming_wf.as_posix())
log.info(f"Project exported without increment to {incoming_wf.as_posix()}: {exported}")


def open_file(filepath):
Expand All @@ -60,6 +104,24 @@ def open_file(filepath):
file = os.path.basename(filepath)
fname, _ = os.path.splitext(file)

# handle project db override if set
project_name = get_current_project_name()
settings = get_project_settings(project_name)
override_is_valid = True
if settings["resolve"]["project_db"].get("enabled", False):
log.info("Handling project database override...")
overrides = settings["resolve"]["project_db"]
override_is_valid = handle_project_db_override(
project_name, overrides
)
if not override_is_valid:
return False

if settings["resolve"]["project_db"]["db_type"] == "Disk":
handle_local_vs_exported_project(
settings, project_name, filepath
)

Comment on lines +107 to +124
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says the local-vs-exported workfile comparison “only triggers when using Disk type database”, but the implementation only runs handle_local_vs_exported_project when the database override setting is enabled. If the intended behavior is to compare for all Disk databases (even without override), the gating condition should be adjusted; otherwise the PR description should be updated to reflect that this is opt-in via the override setting.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah pretty much the entire PR is opt-in.
handle_local_vs_exported_project only runs on db_type="Disk" because otherwise a shared database is used. in that case there is no need for comparing workfiles as every user is guaranteed to work on the same.

try:
# load project from input path
resolve_project = project_manager.LoadProject(fname)
Expand Down Expand Up @@ -93,5 +155,105 @@ def current_file():
return os.path.normpath(current_file_path)


def handle_local_vs_exported_project(settings, project_name, file_path):
project_manager = get_project_manager()

file_name = Path(file_path).stem
if settings["resolve"]["project_db"].get("use_db_project_folder", False):
db_project = get_local_database_root() / project_name / file_name / "Project.db"
else:
db_project = get_local_database_root() / file_name / "Project.db"

if not db_project.exists():
log.warning(f"Project `{file_name}` does not exist in local database. Aborting timestamp comparison.")
return

mtime_drp = Path(file_path).stat().st_mtime
mtime_dbp = db_project.stat().st_mtime if db_project.exists() else 0
if mtime_drp > mtime_dbp:
choice = ProjectImportChooser(mtime_drp, mtime_dbp).exec_()
if choice == QtWidgets.QMessageBox.Ok:
proj = project_manager.LoadProject(file_name)
Comment thread
tweak-wtf marked this conversation as resolved.
if not proj:
log.warning(f"Failed to load project `{file_name}` for import. Aborting.")
return
sha = hashlib.sha1(
f"{file_name}_{time.time()}".encode("utf-8")
).hexdigest()[:6]
proj_bkp_name = f"{proj.GetName()}_BKP_{sha}"
proj.SetName(proj_bkp_name)
project_manager.SaveProject()
project_manager.CloseProject(proj_bkp_name)
project_manager.ImportProject(file_path)
Comment thread
tweak-wtf marked this conversation as resolved.


def handle_project_db_override(project_name, settings) -> bool:
project_manager = get_project_manager()

available_dbs = project_manager.GetDatabaseList() or []
curr_db = project_manager.GetCurrentDatabase()

valid_db_settings = False
for available_db in available_dbs:
if available_db["DbType"] == settings["db_type"]:
if available_db["DbName"] == settings["db_name"]:
# NOTE: Disk databases don't return IP address, so i consider them valid as long as type and name match
if settings["db_type"] == "Disk":
valid_db_settings = True
break
elif available_db.get("IpAddress", "") == settings.get("db_ip", ""):
valid_db_settings = True
break

if not valid_db_settings:
DatabaseMisconfigurationWarning(
settings, available_dbs
).exec_()
return False

# check if we're already in the right database
# reloading the database causes projects to not increment correctly anymore
curr_db_valid = True
if settings["db_type"] != curr_db.get("DbType", ""):
curr_db_valid = False
if settings["db_name"] != curr_db.get("DbName", ""):
curr_db_valid = False
if settings["db_type"] != "Disk" and (
settings["db_ip"] != curr_db.get("IpAddress", "127.0.0.1")
):
curr_db_valid = False

if not curr_db_valid:
db_parms = {
"DbType": settings["db_type"],
"DbName": settings["db_name"],
}
if settings["db_type"] != "Disk":
db_parms["IpAddress"] = settings["db_ip"]
log.info(f"Setting Project Database with Parameters: {db_parms}")
project_manager.SetCurrentDatabase(db_parms)
else:
log.info(f"Using current Project Database: {curr_db}")

if settings.get("use_db_project_folder", False):
set_project_manager_to_folder_name(project_name)

return True


def get_local_database_root() -> Path:
# does anyone use resolve users other than guest?
if sys.platform == "win32":
result = (
Path(os.getenv("APPDATA"))
/ "Blackmagic Design" / "DaVinci Resolve" / "Support"
/ "Resolve Project Library" / "Resolve Projects"
/ "Users" / "guest" / "Projects"
)
Comment on lines +244 to +252
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_local_database_root does Path(os.getenv("APPDATA")); if APPDATA is unset this raises a TypeError and will break workfile open/save. Fetch APPDATA into a variable and raise a clear error (or handle the missing env var) before constructing the Path.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i never really had a case on windows where $APPDATA wasn't set 👀

else:
raise NotImplementedError(f"Database path for platform {sys.platform} is not implemented yet.")
return result
Comment thread
tweak-wtf marked this conversation as resolved.


def work_root(session):
return os.path.normpath(session["AYON_WORKDIR"]).replace("\\", "/")
Loading
Loading