Skip to content

Commit 11fba14

Browse files
committed
Merge branch 'main' into fix/utf-8
2 parents 384b33c + 9de334c commit 11fba14

7 files changed

Lines changed: 150 additions & 121 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
- Play GIFs and Play GIFs Unfocused options (#212 by @Willy-JL):
1515
- Saves a lot of VRAM if completely disabled, no GIFs play and only first frame is loaded
1616
- Saves CPU/GPU usage by redrawing less if disabled when unfocused, but still uses same VRAM
17-
- Tabs can not be reordered by dragging (by @Willy-JL)
17+
- Tabs can now be reordered by dragging (by @Willy-JL)
1818

1919
### Updated:
2020
- New notification system with buttons and better platform support, option to include banner image in update notifs (#220 by @Willy-JL)

common/structs.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,18 @@ def __init__(self, proc: asyncio.subprocess.Process):
107107
self.proc = proc
108108
self.daemon = DaemonProcess(proc)
109109

110+
async def is_alive(self):
111+
await asyncio.sleep(0)
112+
return self.proc.returncode is None
113+
110114
async def get_async(self):
111115
assert self.proc.stdout
112-
while self.proc.returncode is None and not self.proc.stdout.at_eof():
116+
while await self.is_alive() and not self.proc.stdout.at_eof():
113117
line = await self.proc.stdout.readline()
114118
try:
115119
return json.loads(line)
116120
except json.JSONDecodeError:
117121
pass
118-
await asyncio.sleep(0)
119122
raise self.DaemonPipeExit()
120123

121124
def put(self, data: dict | list | str):

external/imagehelper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def post_draw(draw_time: float):
127127
if globals.settings.unload_offscreen_images:
128128
hidden = globals.gui.minimized or globals.gui.hidden
129129
for image in ImageHelper.instances:
130-
if hidden or not image.shown:
130+
if image.loaded and (hidden or not image.shown):
131131
unload_queue.append(image)
132132
else:
133133
image.shown = False
@@ -716,7 +716,7 @@ def apply(self, apply_time_max: float):
716716
def unload(self):
717717
if self.loaded:
718718
if self.texture_ids:
719-
gl.glDeleteTextures([self.texture_ids])
719+
gl.glDeleteTextures(self.texture_ids)
720720
self.texture_ids.clear()
721721
if self.textures:
722722
apply_queue.remove(self)

indexer/watcher.py

Lines changed: 80 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
WATCH_UPDATES_INTERVAL = dt.timedelta(minutes=5).total_seconds()
1717
WATCH_UPDATES_CATEGORIES = f95zone.LATEST_CATEGORIES
18+
WATCH_UPDATES_PAGES = 4
1819
WATCH_VERSIONS_INTERVAL = dt.timedelta(hours=12).total_seconds()
1920
WATCH_VERSIONS_CHUNK_SIZE = 1000
2021

@@ -51,88 +52,89 @@ async def watch_updates():
5152
invalidate_cache = cache.redis.pipeline()
5253

5354
for category in WATCH_UPDATES_CATEGORIES:
54-
logger.info(f"Poll category {category}")
55-
56-
cached_data = cache.redis.pipeline()
57-
58-
try:
59-
async with f95zone.session.get(
60-
f95zone.LATEST_URL.format(
61-
cmd="list",
62-
cat=category,
63-
page=1,
64-
sort="date",
65-
rows=90,
66-
ts=int(time.time()),
67-
),
68-
cookies=f95zone.cookies,
69-
) as req:
70-
res = await req.read()
71-
except Exception as exc:
72-
if index_error := f95zone.check_error(exc, logger):
55+
for page in range(1, WATCH_UPDATES_PAGES + 1):
56+
logger.info(f"Poll category {category} page {page}")
57+
58+
cached_data = cache.redis.pipeline()
59+
60+
try:
61+
async with f95zone.session.get(
62+
f95zone.LATEST_URL.format(
63+
cmd="list",
64+
cat=category,
65+
page=page,
66+
sort="date",
67+
rows=90,
68+
ts=int(time.time()),
69+
),
70+
cookies=f95zone.cookies,
71+
) as req:
72+
res = await req.read()
73+
except Exception as exc:
74+
if index_error := f95zone.check_error(exc, logger):
75+
raise Exception(index_error)
76+
raise
77+
78+
if index_error := f95zone.check_error(res, logger):
7379
raise Exception(index_error)
74-
raise
75-
76-
if index_error := f95zone.check_error(res, logger):
77-
raise Exception(index_error)
7880

79-
try:
80-
updates = json.loads(res)
81-
except Exception:
82-
raise Exception(f"Latest updates returned invalid JSON: {res}")
83-
if index_error := f95zone.check_error(updates, logger):
84-
raise Exception(index_error)
85-
86-
# We compare version strings to detect updates
87-
# But also make a hash of other attributes to detect metadata changes
88-
# We don't save these values directly because we parse from thread content instead
89-
# But using this meta hash allows to discover metadata changes sooner
90-
names = []
91-
current_data = []
92-
for update in updates["msg"]["data"]:
93-
name = cache.NAME_FORMAT.format(id=update["thread_id"])
94-
names.append(name)
95-
cached_data.hmget(name, "version", cache.HASHED_META)
96-
version = update["version"]
97-
if version == "Unknown":
98-
version = None
99-
meta = (
100-
update["title"],
101-
update["creator"],
102-
update["prefixes"],
103-
update["tags"],
104-
round(update["rating"], 1),
105-
update["cover"],
106-
update["screens"],
107-
parser.datestamp(update["ts"]),
108-
)
109-
meta = hashlib.md5(json.dumps(meta).encode()).hexdigest()
110-
current_data.append((version, meta))
111-
112-
cached_data = await cached_data.execute()
113-
114-
assert len(names) == len(current_data) == len(cached_data)
115-
for name, (version, meta), (cached_version, cached_meta) in zip(
116-
names, current_data, cached_data
117-
):
118-
if cached_version is None:
119-
continue
120-
121-
version_outdated = version and version != cached_version
122-
meta_outdated = meta != cached_meta
81+
try:
82+
updates = json.loads(res)
83+
except Exception:
84+
raise Exception(f"Latest updates returned invalid JSON: {res}")
85+
if index_error := f95zone.check_error(updates, logger):
86+
raise Exception(index_error)
12387

124-
if version_outdated or meta_outdated:
125-
# Delete version too to avoid watch_versions() picking it up as mismatch
126-
invalidate_cache.hdel(name, cache.LAST_CACHED, "version")
127-
invalidate_cache.hset(name, cache.HASHED_META, meta)
128-
logger.info(
129-
f"Updates: Invalidating cache for {name}"
130-
+ (
131-
f" ({cached_version!r} -> {version!r})"
132-
if version_outdated
133-
else " (meta changed)"
134-
)
88+
# We compare version strings to detect updates
89+
# But also make a hash of other attributes to detect metadata changes
90+
# We don't save these values directly because we parse from thread content instead
91+
# But using this meta hash allows to discover metadata changes sooner
92+
names = []
93+
current_data = []
94+
for update in updates["msg"]["data"]:
95+
name = cache.NAME_FORMAT.format(id=update["thread_id"])
96+
names.append(name)
97+
cached_data.hmget(name, "version", cache.HASHED_META)
98+
version = update["version"]
99+
if version == "Unknown":
100+
version = None
101+
meta = (
102+
update["title"],
103+
update["creator"],
104+
update["prefixes"],
105+
update["tags"],
106+
round(update["rating"], 1),
107+
update["cover"],
108+
update["screens"],
109+
parser.datestamp(update["ts"]),
135110
)
111+
meta = hashlib.md5(json.dumps(meta).encode()).hexdigest()
112+
current_data.append((version, meta))
113+
114+
cached_data = await cached_data.execute()
115+
116+
assert len(names) == len(current_data) == len(cached_data)
117+
for name, (version, meta), (cached_version, cached_meta) in zip(
118+
names, current_data, cached_data
119+
):
120+
if cached_version is None:
121+
continue
122+
123+
version_outdated = version and version != cached_version
124+
meta_outdated = meta != cached_meta
125+
126+
if version_outdated or meta_outdated:
127+
# Delete version too to avoid watch_versions() picking it up as mismatch
128+
invalidate_cache.hdel(name, cache.LAST_CACHED, "version")
129+
invalidate_cache.hset(name, cache.HASHED_META, meta)
130+
logger.info(
131+
f"Updates: Invalidating cache for {name}"
132+
+ (
133+
f" ({cached_version!r} -> {version!r})"
134+
if version_outdated
135+
else " (meta changed)"
136+
)
137+
)
136138

137139
if len(invalidate_cache):
138140
result = await invalidate_cache.execute()

modules/gui.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,7 @@ def __init__(self):
271271
imgui.WINDOW_NO_SCROLL_WITH_MOUSE
272272
)
273273
self.tabbar_flags: int = (
274-
imgui.TAB_BAR_FITTING_POLICY_SCROLL |
275-
imgui.TAB_BAR_REORDERABLE
274+
imgui.TAB_BAR_FITTING_POLICY_SCROLL
276275
)
277276
self.game_list_table_flags: int = (
278277
imgui.TABLE_SCROLL_Y |
@@ -310,7 +309,6 @@ def __init__(self):
310309
self.watermark_text = f"F95Checker {globals.version_name}{'' if not globals.release else ' by WillyJL'}"
311310

312311
# Variables
313-
self.hidden = False
314312
self.focused = True
315313
self.minimized = False
316314
self.filtering = False
@@ -326,6 +324,7 @@ def __init__(self):
326324
self.recalculate_ids = True
327325
self.current_tab: Tab = None
328326
self.selected_games_count = 0
327+
self.dragging_tab: Tab = None
329328
self.game_hitbox_click = False
330329
self.hovered_game: Game = None
331330
self.filters: list[Filter] = []
@@ -801,26 +800,28 @@ def drop_callback(self, window: glfw._GLFWwindow, items: list[str]):
801800
elif path.suffix and path.suffix.lower() == ".url":
802801
async_thread.run(api.import_url_shortcut(path))
803802

803+
@property
804+
def hidden(self):
805+
return not glfw.get_window_attrib(self.window, glfw.VISIBLE)
806+
804807
def hide(self, *_, **__):
805808
if threading.current_thread() is not threading.main_thread():
806809
self.call_soon.append(self.hide)
807810
self.screen_pos = glfw.get_window_pos(self.window)
808811
glfw.hide_window(self.window)
809-
self.hidden = True
810812
self.tray.update_status()
811813

812814
def show(self, *_, **__):
813815
if threading.current_thread() is not threading.main_thread():
814816
self.call_soon.append(self.show)
815817
self.bg_mode_timer = None
816818
self.bg_mode_notifs_timer = None
817-
if not self.hidden:
818-
glfw.hide_window(self.window)
819+
# if not self.hidden:
820+
# glfw.hide_window(self.window)
819821
glfw.show_window(self.window)
820822
if utils.validate_geometry(*self.screen_pos, *self.prev_size):
821823
glfw.set_window_pos(self.window, *self.screen_pos)
822824
glfw.focus_window(self.window)
823-
self.hidden = False
824825
self.tray.update_status()
825826

826827
def scaled(self, size: int | float):
@@ -876,7 +877,7 @@ def main_loop(self):
876877
cursor = imgui.get_mouse_cursor()
877878
any_hovered = imgui.is_any_item_hovered()
878879
win_hovered = glfw.get_window_attrib(self.window, glfw.HOVERED)
879-
if not self.hidden and not self.minimized and (self.focused or globals.settings.render_when_unfocused):
880+
if first_frame or (not self.hidden and not self.minimized): # Visible
880881

881882
# Scroll modifiers (must be before new_frame())
882883
imgui.io.mouse_wheel *= globals.settings.scroll_amount
@@ -901,6 +902,7 @@ def main_loop(self):
901902
or (imagehelper.redraw and globals.settings.play_gifs and (self.focused or globals.settings.play_gifs_unfocused))
902903
or imgui.io.mouse_wheel or self.input_chars or any(imgui.io.mouse_down) or any(imgui.io.keys_down)
903904
or (prev_mouse_pos != mouse_pos and (prev_win_hovered or win_hovered))
905+
or (self.focused or globals.settings.render_when_unfocused)
904906
or imagehelper.apply_queue or imagehelper.unload_queue
905907
or prev_scaling != globals.settings.interface_scaling
906908
or prev_minimized != self.minimized
@@ -914,8 +916,9 @@ def main_loop(self):
914916
)
915917
if draw:
916918
draw_next = max(draw_next, imgui.io.delta_time + 1.0) # Draw for at least next half second
917-
if draw_next > 0.0:
918-
draw_next -= imgui.io.delta_time
919+
if draw_next > 0.0: # Visible and drawing
920+
if not first_frame:
921+
draw_next -= imgui.io.delta_time
919922
draw_start = time.perf_counter()
920923

921924
# Reactive mouse cursors
@@ -1023,11 +1026,14 @@ def main_loop(self):
10231026
# Wait idle time
10241027
glfw.swap_buffers(self.window)
10251028
if first_frame:
1026-
glfw.show_window(self.window)
1029+
if not globals.settings.start_in_background:
1030+
glfw.show_window(self.window)
10271031
first_frame = False
1028-
else:
1032+
else: # Visible but not drawing
10291033
time.sleep(1 / 15)
1030-
else:
1034+
else: # Not visible
1035+
# Unload images if necessary
1036+
imagehelper.post_draw(0)
10311037
# Tray bg mode and not paused
10321038
if self.hidden and not self.bg_mode_paused and not utils.is_refreshing():
10331039
if not self.bg_mode_timer:
@@ -2908,16 +2914,23 @@ def draw_tabbar(self):
29082914
self.current_tab = display_tab
29092915
self.tick_list_columns()
29102916
save_new_tab = False
2917+
elif self.dragging_tab:
2918+
# Keep current tab while resetting tabbar due to dragging
2919+
save_new_tab = False
2920+
select_tab = True
29112921
else:
29122922
save_new_tab = True
29132923
new_tab = None
29142924
if Tab.instances and not (globals.settings.filter_all_tabs and self.filtering):
2915-
if imgui.begin_tab_bar("###tabbar", flags=self.tabbar_flags):
2925+
if imgui.begin_tab_bar(
2926+
f"###tabbar_{','.join(str(tab.id) for tab in Tab.instances)}",
2927+
flags=self.tabbar_flags
2928+
):
29162929
hide = globals.settings.hide_empty_tabs
29172930
count = len(self.show_games_ids.get(None, ()))
29182931
if (count or not hide) and imgui.begin_tab_item(
29192932
f"{Tab.first_tab_label()} ({count})###tab_-1",
2920-
flags=imgui.TAB_ITEM_NO_REORDER
2933+
flags=imgui.TAB_ITEM_NONE
29212934
)[0]:
29222935
new_tab = None
29232936
imgui.end_tab_item()
@@ -2939,14 +2952,18 @@ def draw_tabbar(self):
29392952
imgui.end_tab_item()
29402953
if tab.color:
29412954
imgui.pop_style_color(4)
2942-
if imgui.is_item_active():
2955+
if self.dragging_tab is tab and imgui.is_mouse_released():
2956+
self.dragging_tab = None
2957+
elif imgui.is_item_active() or self.dragging_tab is tab:
29432958
mouse_pos = imgui.get_mouse_pos()
2944-
if tab_i > 0 and mouse_pos.x < imgui.get_item_rect_min().x:
2959+
if tab_i > 0 and imgui.get_item_rect_min().x > 0 and mouse_pos.x < imgui.get_item_rect_min().x:
29452960
if imgui.get_mouse_drag_delta().x < 0:
2961+
self.dragging_tab = tab
29462962
swap = (tab_i, tab_i - 1)
29472963
imgui.reset_mouse_drag_delta()
2948-
elif tab_i < len(Tab.instances) - 1 and mouse_pos.x > imgui.get_item_rect_max().x:
2964+
elif tab_i < len(Tab.instances) - 1 and imgui.get_item_rect_max().x > 0 and mouse_pos.x > imgui.get_item_rect_max().x:
29492965
if imgui.get_mouse_drag_delta().x > 0:
2966+
self.dragging_tab = tab
29502967
swap = (tab_i, tab_i + 1)
29512968
imgui.reset_mouse_drag_delta()
29522969
context_id = f"###tab_{tab.id}_context"

0 commit comments

Comments
 (0)