Skip to content

Commit 4076f65

Browse files
committed
Added natural sorting for images in chapter loading. Added option to invert mouse scroll. Attempted to implement dynamic loading. WebtoonReader will now load 5 images at a time. Plan in future to implement loading previous 5 images upon scrolling to the top.
1 parent 14ba7de commit 4076f65

File tree

3 files changed

+149
-41
lines changed

3 files changed

+149
-41
lines changed

CustomScroller.py

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Referenced from: https://stackoverflow.com/a/56046307
22

33
import tkinter as tk
4-
import os
4+
import os, re
55
from PIL import ImageTk, Image
66

77
# Custom infinite seamless vertical image scroller using Tkinter Canvas and Scrollbar
@@ -12,18 +12,24 @@ def __init__(self, master=None, **kw):
1212
self.path = kw.pop('path', None)
1313
self.width = kw.pop('width', None)
1414
self.height = kw.pop('height', None)
15+
self.bg = kw.pop('bg', None)
1516
self.scroll_speed = kw.pop('speed', None)
17+
self.image_load = kw.pop('load', None)
18+
self.invert = kw.pop('invert', None)
1619
sw = kw.pop('scrollbarwidth', 10)
1720
super(ImageScroller, self).__init__(master=master, **kw)
18-
self.canvas = tk.Canvas(self, width=self.width, height=self.height, highlightthickness=0, **kw)
21+
self.canvas = tk.Canvas(self, width=self.width, height=self.height, bg=self.bg, highlightthickness=0, **kw)
1922

20-
21-
# List of chapter images
23+
# List of images
2224
self.images = []
2325

26+
self.scroll_flag = True
27+
self.image_index = 0
28+
29+
2430
# Fill the frame with images
2531
if self.path != "":
26-
self.fill()
32+
self.fill(self.image_index)
2733

2834
# Create vertical scrollbar
2935
self.v_scroll = tk.Scrollbar(self, orient='vertical', width=sw)
@@ -61,7 +67,16 @@ def mouse_scroll(self, event):
6167
elif event.num == 5 or event.delta < 0:
6268
self.canvas.yview_scroll(self.scroll_speed, "units" )
6369

64-
#if self.v_scroll.get()[1] == 1.0
70+
if self.v_scroll.get()[1] == 1:
71+
if self.scroll_flag and self.image_index + self.image_load < len(os.listdir(self.path)):
72+
self.image_index += self.image_load
73+
self.fill(self.image_index)
74+
self.scroll_flag = False
75+
76+
if self.v_scroll.get()[1] != 0 and self.v_scroll.get()[1] != 1:
77+
self.scroll_flag = True
78+
79+
6580

6681

6782
# Mouse drag handling
@@ -73,41 +88,62 @@ def start_scroll(self, event):
7388

7489
# https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/cursors.html
7590
self.canvas.config(cursor="hand2")
76-
91+
7792

7893
def update_scroll(self, event):
7994
deltaX = event.x - self._starting_drag_position[0]
8095
deltaY = event.y - self._starting_drag_position[1]
8196
self.canvas.xview('scroll', deltaX, 'units')
82-
self.canvas.yview('scroll', deltaY, 'units')
97+
98+
if self.invert:
99+
self.canvas.yview('scroll', -deltaY, 'units')
100+
else:
101+
self.canvas.yview('scroll', deltaY, 'units')
83102
self._starting_drag_position = (event.x, event.y)
84-
85103

104+
if self.v_scroll.get()[1] == 1:
105+
if self.scroll_flag and self.image_index + self.image_load < len(os.listdir(self.path)):
106+
self.image_index += self.image_load
107+
self.fill(self.image_index)
108+
self.scroll_flag = False
109+
110+
86111
def stop_scroll(self, event):
87112
self.canvas.config(xscrollincrement=0)
88113
self.canvas.config(yscrollincrement=0)
89114
self.canvas.config(cursor="")
90115

116+
# Fills the frame with images from the folder path
117+
def fill(self, index):
91118

119+
# Free memory from previous load
120+
self.canvas.delete('all')
121+
self.images.clear()
92122

93-
# Fills the frame with images from the folder path
94-
def fill(self):
95-
if os.path.exists(self.path):
96-
# Gets all images from directory and adds to list
97-
for name in os.listdir(self.path):
98-
img = Image.open(os.path.join(self.path, name))
99-
100-
# Rescales all images
101-
if img.width != self.width:
102-
scale = img.height / img.width
103-
img = img.resize((self.width, int(self.width * scale)), Image.Resampling.LANCZOS)
104-
105-
# Adds to list to prevent garbage collection
106-
self.images.append(ImageTk.PhotoImage(img))
123+
for i in range(self.image_load):
124+
if index + i >= len(os.listdir(self.path)):
125+
break
107126

108-
height = 0
109-
# Creates each image and updates the height to place next image
110-
for i in range(len(self.images)):
111-
self.canvas.create_image(0, height, anchor=tk.NW, image=self.images[i])
112-
height = height + self.images[i].height()
113-
127+
img = Image.open(os.path.join(self.path, self.natural_sort(os.listdir(self.path))[index + i]))
128+
129+
# Rescales all images to width
130+
if img.width != self.width:
131+
scale = img.height / img.width
132+
img = img.resize((self.width, int(self.width * scale)), Image.Resampling.LANCZOS)
133+
134+
# Adds to list to prevent garbage collection
135+
self.images.append(ImageTk.PhotoImage(img))
136+
137+
height = 0
138+
for i in range(len(self.images)):
139+
self.canvas.create_image(0, height, anchor=tk.NW, image=self.images[i])
140+
height = height + self.images[i].height()
141+
142+
self.canvas.yview_moveto(-1)
143+
144+
145+
# Natural sort files
146+
def natural_sort(self, l):
147+
convert = lambda text: int(text) if text.isdigit() else text.lower()
148+
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
149+
return sorted(l, key=alphanum_key)

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
## 🚀 &nbsp; Features
2121
* Comfortably read locally stored manga/webtoon
2222
* Automatically bookmarks each manga/webtoon
23-
* Configure manga/webtoon width, height, and scroll speed
23+
* Configure manga/webtoon width, height, scroll speed, and invert mouse scroll
2424
* Clean and intuitive interface
2525

2626
## 📝 &nbsp; How To Use
@@ -77,6 +77,8 @@ WebtoonReader will bookmark where you left off on each Manga after each session.
7777

7878
## 🤖 &nbsp; To Do
7979
* Squash all the bugs!
80+
* Automatically load next chapter (if possible) after finishing a chapter
81+
* Add feature to dynamically load previous pages of a chapter
8082

8183
## 📘 &nbsp; License
8284
WebtoonReader is released under the [MIT license](https://github.com/Aeonss/WebtoonReader/blob/master/LICENSE.md).

WebtoonReader.py

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616

1717
SETTINGS_FILE = os.path.join(Path.home(), "webtoonreader_settings.json")
1818

19-
# To do
20-
# Add bookmarks of where you left off on a manga
2119

2220
class WebtoonReader:
2321
def __init__(self):
2422
# Create settings file in the home directory if it does not exist
2523
if not os.path.isfile(SETTINGS_FILE):
2624
with open(SETTINGS_FILE, "a") as f:
27-
json.dump({"library" : os.getcwd(), "width" : 720, "height" : 800, "scroll_speed" : 3, "recent_chapter" : "", "recent_chapter_index" : ""}, f)
25+
json.dump({"library" : os.getcwd(), "width" : 720, "height" : 800, "scroll_speed" : 3, "recent_chapter" : "", "recent_chapter_index" : "", "load" : 5, "invert_scroll" : False}, f)
2826
f.close()
2927

3028

@@ -35,6 +33,8 @@ def __init__(self):
3533
self.width = get_json('width')
3634
self.height = get_json('height')
3735
self.scroll_speed = get_json('scroll_speed')
36+
self.load = get_json('load')
37+
self.invert_scroll = get_json('invert_scroll')
3838

3939

4040
# Center Window
@@ -44,7 +44,14 @@ def __init__(self):
4444

4545
# ImageScroller
4646
chapter_path = get_json('recent_chapter')
47-
self.frame = ImageScroller(self.window, path=chapter_path, scrollbarwidth=15, width=self.width, height=self.height, speed=self.scroll_speed)
47+
self.frame = ImageScroller(self.window,
48+
path=chapter_path,
49+
scrollbarwidth=15,
50+
width=self.width,
51+
height=self.height,
52+
speed=self.scroll_speed,
53+
load=self.load,
54+
invert=self.invert_scroll)
4855
self.frame.pack()
4956
manga = os.path.basename(os.path.dirname(chapter_path))
5057
self.window.title("[WebtoonReader] - " + manga + ": " + os.path.basename(chapter_path))
@@ -115,7 +122,14 @@ def create_chapter(self, path):
115122

116123
# Updates the image scroller
117124
self.frame.destroy()
118-
self.frame = ImageScroller(self.window, path=chapter_path, scrollbarwidth=15, width=self.width, height=self.height, speed=self.scroll_speed)
125+
self.frame = ImageScroller(self.window,
126+
path=chapter_path,
127+
scrollbarwidth=15,
128+
width=self.width,
129+
height=self.height,
130+
speed=self.scroll_speed,
131+
load=self.load,
132+
invert=self.invert_scroll)
119133
self.frame.pack()
120134

121135
# Updates settings json
@@ -145,7 +159,14 @@ def next_chapter(self):
145159

146160
# Updates the image scroller
147161
self.frame.destroy()
148-
self.frame = ImageScroller(self.window, path=chapter_path, scrollbarwidth=15, width=self.width, height=self.height, speed=self.scroll_speed)
162+
self.frame = ImageScroller(self.window,
163+
path=chapter_path,
164+
scrollbarwidth=15,
165+
width=self.width,
166+
height=self.height,
167+
speed=self.scroll_speed,
168+
load=self.load,
169+
invert=self.invert_scroll)
149170
self.frame.pack()
150171

151172
# Updates the settings json
@@ -175,7 +196,14 @@ def prev_chapter(self):
175196

176197
# Updates the image scroller
177198
self.frame.destroy()
178-
self.frame = ImageScroller(self.window, path=chapter_path, scrollbarwidth=15, width=self.width, height=self.height, speed=self.scroll_speed)
199+
self.frame = ImageScroller(self.window,
200+
path=chapter_path,
201+
scrollbarwidth=15,
202+
width=self.width,
203+
height=self.height,
204+
speed=self.scroll_speed,
205+
load=self.load,
206+
invert=self.invert_scroll)
179207
self.frame.pack()
180208

181209
# Updates the settings json
@@ -220,24 +248,59 @@ def set_settings(self):
220248
self.scroll_speed_slider.set(self.scroll_speed)
221249
self.scroll_speed_slider.pack(pady=10)
222250
self.scroll_speed_slider.bind("<ButtonRelease-1>", self.update_speed)
251+
252+
invert_label = tk.Label(settings, text="Invert Scroll").pack(pady=10)
253+
self.invert_checkbox = tk.IntVar()
254+
self.checkbutton = tk.Checkbutton(settings, variable=self.invert_checkbox)
255+
if self.invert_scroll:
256+
self.checkbutton.select()
257+
else:
258+
self.checkbutton.deselect()
259+
self.checkbutton.pack(pady=10)
260+
self.checkbutton.bind("<ButtonRelease-1>", self.update_invert)
223261

224262
settings.mainloop()
225263

226264
# Updates width in settings json
227265
def update_width(self, e):
228266
self.width = self.width_slider.get()
229267
update_json('width', self.width_slider.get())
268+
self.restart_canvas()
230269

231270
# Updates height in settings json
232271
def update_height(self, e):
233272
self.height = self.height_slider.get()
234-
update_json('height', self.height_slider.get())
273+
update_json('height', self.height_slider.get())
274+
self.restart_canvas()
235275

236276
# Updates scroll speed in settings json
237277
def update_speed(self, e):
238278
self.scroll_speed = self.scroll_speed_slider.get()
239279
update_json('scroll_speed', self.scroll_speed_slider.get())
280+
self.restart_canvas()
240281

282+
def update_invert(self, e):
283+
if self.invert_checkbox.get() == 0:
284+
update_json('invert_scroll', True)
285+
self.invert_scroll = True
286+
else:
287+
update_json('invert_scroll', False)
288+
self.invert_scroll = False
289+
self.restart_canvas()
290+
291+
292+
def restart_canvas(self):
293+
chapter_path = get_json('recent_chapter')
294+
self.frame.destroy()
295+
self.frame = ImageScroller(self.window,
296+
path=chapter_path,
297+
scrollbarwidth=15,
298+
width=self.width,
299+
height=self.height,
300+
speed=self.scroll_speed,
301+
load=self.load,
302+
invert=self.invert_scroll)
303+
self.frame.pack()
241304

242305
# Update the json value if key exists, otherwise adds to json
243306
def update_json(key, value):
@@ -260,7 +323,14 @@ def get_json(key):
260323
json_file.close()
261324
return value
262325
json_file.close()
263-
return None
326+
327+
# Recreate the settings file if its broken
328+
if os.path.isfile(SETTINGS_FILE):
329+
os.remove(SETTINGS_FILE)
330+
with open(SETTINGS_FILE, "a") as f:
331+
json.dump({"library" : os.getcwd(), "width" : 720, "height" : 800, "scroll_speed" : 3, "recent_chapter" : "", "recent_chapter_index" : "", "load" : 5, "invert_scroll" : False}, f)
332+
f.close()
333+
return get_json(key)
264334

265335

266336
# Returns a natural sorted list of absolute paths directories
@@ -269,12 +339,12 @@ def abslistdir(path):
269339
for root, dirs, files in os.walk(path):
270340
for dir in dirs:
271341
list.append(os.path.join(root, dir).replace("\\", "/"))
272-
list.sort(key=natural_sort_key)
342+
list.sort(key=natural_sort)
273343
return list
274344

275345

276346
# Natural sort a list
277-
def natural_sort_key(list):
347+
def natural_sort(list):
278348
return [int(text) if text.isdigit() else text.lower()
279349
for text in re.split(re.compile('([0-9]+)'), list)]
280350

0 commit comments

Comments
 (0)