BloodyInside commited on
Commit
cd23862
·
1 Parent(s): 0ab8c90
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. backend/__pycache__/urls.cpython-312.pyc +0 -0
  2. backend/api/__pycache__/queue.cpython-312.pyc +0 -0
  3. backend/api/__pycache__/stream_file.cpython-312.pyc +0 -0
  4. backend/api/__pycache__/web_scrap.cpython-312.pyc +0 -0
  5. backend/api/__pycache__/web_scrape.cpython-312.pyc +0 -0
  6. backend/api/queue.py +2 -1
  7. backend/api/stream_file.py +2 -1
  8. backend/api/{web_scrap.py → web_scrape.py} +69 -16
  9. backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc +0 -0
  10. backend/invoke_worker/chapter_queue.py +119 -82
  11. backend/migrations/0001_initial.py +3 -3
  12. backend/migrations/0002_remove_requestcache_room.py +0 -17
  13. backend/migrations/0002_webscrapegetcovercache.py +24 -0
  14. backend/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  15. backend/migrations/__pycache__/0002_remove_requestcache_room.cpython-312.pyc +0 -0
  16. backend/migrations/__pycache__/0002_webscrapegetcovercache.cpython-312.pyc +0 -0
  17. backend/models/__pycache__/model_1.cpython-312.pyc +0 -0
  18. backend/models/__pycache__/model_cache.cpython-312.pyc +0 -0
  19. backend/models/model_1.py +24 -4
  20. backend/models/model_cache.py +0 -12
  21. backend/module/utils/__pycache__/manage_image.cpython-312.pyc +0 -0
  22. backend/module/utils/manage_image.py +52 -1
  23. backend/module/web_scrap/__pycache__/utils.cpython-312.pyc +0 -0
  24. backend/module/web_scrap/utils.py +0 -1
  25. backend/urls.py +4 -5
  26. core/__pycache__/settings.cpython-312.pyc +0 -0
  27. core/settings.py +21 -32
  28. frontend/app/_layout.tsx +11 -3
  29. frontend/app/bookmark/_layout.tsx +19 -0
  30. frontend/app/bookmark/components/bookmark_component.tsx +87 -0
  31. frontend/app/bookmark/components/comic_component.tsx +85 -0
  32. frontend/app/bookmark/components/widgets/bookmark.tsx +904 -0
  33. frontend/app/bookmark/index.tsx +291 -0
  34. frontend/app/bookmark/stylesheet/styles.tsx +68 -0
  35. frontend/app/explore/index.tsx +4 -4
  36. frontend/app/explore/stylesheet/{show_list_styles.tsx → styles.tsx} +0 -0
  37. frontend/app/index.tsx +1 -1
  38. frontend/app/read/[source]/[comic_id]/[chapter_idx].tsx +33 -57
  39. frontend/app/read/_layout.tsx +0 -1
  40. frontend/app/read/components/chapter_image.tsx +29 -6
  41. frontend/app/read/components/disqus.tsx +1 -6
  42. frontend/app/recent/_layout.tsx +19 -0
  43. frontend/app/recent/components/comic_component.tsx +194 -0
  44. frontend/app/recent/components/widgets/bookmark.tsx +904 -0
  45. frontend/app/recent/index.tsx +151 -0
  46. frontend/app/recent/stylesheet/styles.tsx +63 -0
  47. frontend/app/view/[source]/[comic_id].tsx +131 -115
  48. frontend/app/view/componenets/chapter.tsx +213 -137
  49. frontend/app/view/componenets/widgets/bookmark.tsx +195 -187
  50. frontend/app/view/componenets/widgets/request_chapter.tsx +6 -14
backend/__pycache__/urls.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/urls.cpython-312.pyc and b/backend/__pycache__/urls.cpython-312.pyc differ
 
backend/api/__pycache__/queue.cpython-312.pyc CHANGED
Binary files a/backend/api/__pycache__/queue.cpython-312.pyc and b/backend/api/__pycache__/queue.cpython-312.pyc differ
 
backend/api/__pycache__/stream_file.cpython-312.pyc CHANGED
Binary files a/backend/api/__pycache__/stream_file.cpython-312.pyc and b/backend/api/__pycache__/stream_file.cpython-312.pyc differ
 
backend/api/__pycache__/web_scrap.cpython-312.pyc CHANGED
Binary files a/backend/api/__pycache__/web_scrap.cpython-312.pyc and b/backend/api/__pycache__/web_scrap.cpython-312.pyc differ
 
backend/api/__pycache__/web_scrape.cpython-312.pyc ADDED
Binary file (7.98 kB). View file
 
backend/api/queue.py CHANGED
@@ -9,7 +9,8 @@ from asgiref.sync import sync_to_async
9
 
10
  from backend.module import web_scrap
11
  from backend.module.utils import manage_image
12
- from backend.models.model_cache import SocketRequestChapterQueueCache, ComicStorageCache
 
13
  from core.settings import BASE_DIR
14
  from backend.module.utils import cloudflare_turnstile
15
 
 
9
 
10
  from backend.module import web_scrap
11
  from backend.module.utils import manage_image
12
+ from backend.models.model_cache import SocketRequestChapterQueueCache
13
+ from backend.models.model_1 import ComicStorageCache
14
  from core.settings import BASE_DIR
15
  from backend.module.utils import cloudflare_turnstile
16
 
backend/api/stream_file.py CHANGED
@@ -3,7 +3,8 @@ from core.settings import BASE_DIR
3
  from django_ratelimit.decorators import ratelimit
4
  from django.views.decorators.csrf import csrf_exempt
5
  from backend.module.utils import cloudflare_turnstile
6
- from backend.models.model_cache import SocketRequestChapterQueueCache, ComicStorageCache
 
7
 
8
  import os, json, sys
9
 
 
3
  from django_ratelimit.decorators import ratelimit
4
  from django.views.decorators.csrf import csrf_exempt
5
  from backend.module.utils import cloudflare_turnstile
6
+ from backend.models.model_cache import SocketRequestChapterQueueCache
7
+ from backend.models.model_1 import ComicStorageCache
8
 
9
  import os, json, sys
10
 
backend/api/{web_scrap.py → web_scrape.py} RENAMED
@@ -1,8 +1,8 @@
1
 
2
  import json, environ, requests, os, subprocess
3
- import asyncio, uuid
4
 
5
- from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
6
  from django_ratelimit.decorators import ratelimit
7
  from django.views.decorators.csrf import csrf_exempt
8
  from asgiref.sync import sync_to_async
@@ -12,10 +12,15 @@ from backend.module.utils import manage_image
12
  from backend.models.model_cache import RequestCache
13
  from core.settings import BASE_DIR
14
  from backend.module.utils import cloudflare_turnstile
 
 
 
15
 
16
 
17
  env = environ.Env()
18
 
 
 
19
 
20
  @csrf_exempt
21
  @ratelimit(key='ip', rate='20/m')
@@ -29,22 +34,17 @@ def get_list(request):
29
  page = payload.get("page")
30
  source = payload.get("source")
31
 
 
 
 
 
 
32
  if search.get("text"): DATA = web_scrap.source_control[source].search.scrap(search=search,page=page)
33
  else: DATA = web_scrap.source_control["colamanga"].get_list.scrap(page=page)
34
 
35
  return JsonResponse({"data":DATA})
36
 
37
 
38
- @ratelimit(key='ip', rate='20/m')
39
- def search(request):
40
- # if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
41
- try:
42
- DATA = web_scrap.source_control["colamanga"].search.scrap(search="妖")
43
- return JsonResponse({"data":DATA})
44
- except Exception as e:
45
- return HttpResponseBadRequest(str(e), status=500)
46
-
47
-
48
 
49
  @csrf_exempt
50
  @ratelimit(key='ip', rate='20/m')
@@ -70,11 +70,64 @@ def get_cover(request,source,id,cover_id):
70
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
71
  if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511)
72
 
 
 
 
 
 
73
  try:
74
- DATA = web_scrap.source_control[source].get_cover.scrap(id=id,cover_id=cover_id)
75
- if not DATA: HttpResponseBadRequest('Image Not found!', status=404)
76
- response = HttpResponse(DATA, content_type="image/png")
77
- response['Content-Disposition'] = f'inline; filename="{id}-{cover_id}.png"'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  return response
79
  except Exception as e:
80
  return HttpResponseBadRequest(str(e), status=500)
 
1
 
2
  import json, environ, requests, os, subprocess
3
+ import asyncio, uuid, shutil
4
 
5
+ from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, StreamingHttpResponse
6
  from django_ratelimit.decorators import ratelimit
7
  from django.views.decorators.csrf import csrf_exempt
8
  from asgiref.sync import sync_to_async
 
12
  from backend.models.model_cache import RequestCache
13
  from core.settings import BASE_DIR
14
  from backend.module.utils import cloudflare_turnstile
15
+ from backend.models.model_1 import WebscrapeGetCoverCache
16
+
17
+ from backend.module.utils import directory_info, date_utils
18
 
19
 
20
  env = environ.Env()
21
 
22
+ STORAGE_DIR = os.path.join(BASE_DIR,"storage")
23
+ if not os.path.exists(STORAGE_DIR): os.makedirs(STORAGE_DIR)
24
 
25
  @csrf_exempt
26
  @ratelimit(key='ip', rate='20/m')
 
34
  page = payload.get("page")
35
  source = payload.get("source")
36
 
37
+
38
+
39
+
40
+
41
+
42
  if search.get("text"): DATA = web_scrap.source_control[source].search.scrap(search=search,page=page)
43
  else: DATA = web_scrap.source_control["colamanga"].get_list.scrap(page=page)
44
 
45
  return JsonResponse({"data":DATA})
46
 
47
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  @csrf_exempt
50
  @ratelimit(key='ip', rate='20/m')
 
70
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
71
  if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511)
72
 
73
+ file_path = ""
74
+ file_name = ""
75
+ chunk_size = 8192
76
+ MAX_COVER_STORAGE_SIZE = 10 * 1024 * 1024 * 1024
77
+
78
  try:
79
+ query_result = WebscrapeGetCoverCache.objects.filter(source=source,comic_id=id,cover_id=cover_id).first()
80
+ if (
81
+ query_result
82
+ and os.path.exists(query_result.file_path)
83
+ and query_result.datetime >= date_utils.utc_time().add(-5,'hour').get()
84
+ ):
85
+ file_path = query_result.file_path
86
+ file_name = os.path.basename(file_path)
87
+
88
+ else:
89
+ if not os.path.exists(os.path.join(STORAGE_DIR,"covers")): os.makedirs(os.path.join(STORAGE_DIR,"covers"))
90
+
91
+ while True:
92
+ storage_size = directory_info.GetDirectorySize(directory=os.path.join(STORAGE_DIR,"covers"),max_threads=5)
93
+ if (storage_size >= MAX_COVER_STORAGE_SIZE):
94
+ query_result = WebscrapeGetCoverCache.objects.order_by("datetime").first()
95
+ if (query_result):
96
+ file_path = query_result.file_path
97
+ if os.path.exists(file_path): shutil.rmtree(file_path)
98
+ WebscrapeGetCoverCache.objects.filter(file_path=query_result.file_path).delete()
99
+ else:
100
+ shutil.rmtree(os.path.join(STORAGE_DIR,"covers"))
101
+ break
102
+ else: break
103
+ print(storage_size)
104
+
105
+ DATA = web_scrap.source_control[source].get_cover.scrap(id=id,cover_id=cover_id)
106
+ if not DATA: HttpResponseBadRequest('Image Not found!', status=404)
107
+
108
+ file_path = os.path.join(STORAGE_DIR,"covers",f'{source}-{id}-{cover_id}.png')
109
+ file_name = os.path.basename(file_path)
110
+
111
+ with open(file_path, "wb") as f: f.write(DATA)
112
+
113
+ WebscrapeGetCoverCache(
114
+ file_path=file_path,
115
+ source=source,
116
+ comic_id=id,
117
+ cover_id=cover_id,
118
+ ).save()
119
+
120
+
121
+
122
+ def file_iterator():
123
+ with open(file_path, 'rb') as f:
124
+ while chunk := f.read(chunk_size):
125
+ yield chunk
126
+
127
+ response = StreamingHttpResponse(file_iterator())
128
+ response['Content-Type'] = 'application/octet-stream'
129
+ response['Content-Length'] = os.path.getsize(file_path)
130
+ response['Content-Disposition'] = f'attachment; filename="{file_name}"'
131
  return response
132
  except Exception as e:
133
  return HttpResponseBadRequest(str(e), status=500)
backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc CHANGED
Binary files a/backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc and b/backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc differ
 
backend/invoke_worker/chapter_queue.py CHANGED
@@ -2,7 +2,8 @@ from django_thread import Thread
2
  from time import sleep
3
  from backend.module.utils import date_utils
4
  from django.db import connections
5
- from backend.models.model_cache import SocketRequestChapterQueueCache, ComicStorageCache
 
6
  from core.settings import BASE_DIR
7
  from backend.module import web_scrap
8
  from backend.module.utils import manage_image
@@ -22,6 +23,8 @@ env = environ.Env()
22
 
23
  STORAGE_DIR = os.path.join(BASE_DIR,"storage")
24
  if not os.path.exists(STORAGE_DIR): os.makedirs(STORAGE_DIR)
 
 
25
 
26
  LOG_DIR = os.path.join(BASE_DIR, "log")
27
  if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR)
@@ -37,7 +40,7 @@ class Job(Thread):
37
 
38
  query_result = SocketRequestChapterQueueCache.objects.order_by("datetime").first()
39
  while True:
40
- if (GetDirectorySize(STORAGE_DIR) >= MAX_STORAGE_SIZE):
41
  query_result_2 = ComicStorageCache.objects.order_by("datetime").first()
42
  if (query_result_2):
43
  file_path = query_result_2.file_path
@@ -73,33 +76,44 @@ class Job(Thread):
73
  else:
74
  connections['cache'].close()
75
 
76
- script = []
77
-
78
- input_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),"temp")
 
79
 
80
- if (options.get("translate").get("state") and options.get("colorize")):
81
-
82
- managed_output_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated_colorized")
83
- script = ["python", "-m", "manga_translator", "-v", "--overwrite", "--attempts=3", "--ocr=mocr", "--no-text-lang-skip", "--det-auto-rotate", "--det-gamma-correct", "--colorize=mc2", "--translator=m2m100_big", "-l", f"{options.get("translate").get("target")}", "-i", f"{input_dir}", "-o", f"{managed_output_dir}"]
84
- elif (options.get("translate").get("state") and not options.get("colorize")):
85
-
86
- managed_output_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated")
87
- script = ["python", "-m", "manga_translator", "-v", "--overwrite", "--attempts=3", "--ocr=mocr", "--no-text-lang-skip", "--det-auto-rotate", "--det-gamma-correct", "--translator=m2m100_big", "-l", f"{options.get("translate").get("target")}", "-i", f"{input_dir}", "-o", f"{managed_output_dir}"]
88
- elif (options.get("colorize") and not options.get("translate").get("state")):
89
-
90
- managed_output_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),"colorized")
91
- script = ["python", "-m", "manga_translator", "-v", "--overwrite", "--attempts=3", "--detector=none", "--translator=original", "--colorize=mc2", "--colorization-size=-1", "-i", f"{input_dir}", "-o", f"{managed_output_dir}"]
 
 
 
 
 
 
 
92
 
93
- if target_lang == "ENG": script.append("--manga2eng")
94
 
95
 
96
-
97
- if (options.get("colorize") or options.get("translate").get("state")):
98
  if os.path.exists(input_dir): shutil.rmtree(input_dir)
 
99
  if os.path.exists(managed_output_dir): shutil.rmtree(managed_output_dir)
100
 
101
  job = web_scrap.source_control[source].get_chapter.scrap(comic_id=comic_id,chapter_id=chapter_id,output_dir=input_dir)
 
102
  if job.get("status") == "success":
 
 
 
103
 
104
  with open(os.path.join(LOG_DIR,"image_translator_output.log"), "w") as file:
105
  result = subprocess.run(
@@ -113,7 +127,7 @@ class Job(Thread):
113
  )
114
  if result.returncode != 0: raise Exception("Image Translator Execution error!")
115
  os.makedirs(managed_output_dir,exist_ok=True)
116
- shutil.rmtree(input_dir)
117
 
118
  with zipfile.ZipFile(managed_output_dir + '.zip', 'w') as zipf:
119
  for foldername, subfolders, filenames in os.walk(managed_output_dir):
@@ -139,63 +153,80 @@ class Job(Thread):
139
 
140
  query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first()
141
  channel_name = query_result_3.channel_name if query_result_3 else ""
142
- channel_layer = get_channel_layer()
143
- async_to_sync(channel_layer.send)(channel_name, {
144
- 'type': 'event_send',
145
- 'data': {
146
- "type": "chapter_ready_to_download",
147
- "data": {
148
- "source": source,
149
- "comic_id": comic_id,
150
- "chapter_id": chapter_id,
151
- "chapter_idx": chapter_idx
152
- }
153
- }
154
- })
155
  SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete()
156
- else: raise Exception("Dowload chapter error!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  else:
158
- input_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),"original")
 
 
159
  if os.path.exists(input_dir): shutil.rmtree(input_dir)
 
160
 
161
  job = web_scrap.source_control["colamanga"].get_chapter.scrap(comic_id=comic_id,chapter_id=chapter_id,output_dir=input_dir)
162
-
163
- with zipfile.ZipFile(input_dir + '.zip', 'w') as zipf:
164
- for foldername, subfolders, filenames in os.walk(input_dir):
165
- for filename in filenames:
166
- if filename.endswith(('.png', '.jpg', '.jpeg')):
167
- file_path = os.path.join(foldername, filename)
168
- zipf.write(file_path, os.path.basename(file_path))
169
- shutil.rmtree(input_dir)
170
-
171
- ComicStorageCache(
172
- source = source,
173
- comic_id = comic_id,
174
- chapter_id = chapter_id,
175
- chapter_idx = chapter_idx,
176
- file_path = input_dir + '.zip',
177
- colorize = False,
178
- translate = False,
179
- target_lang = "",
180
-
181
- ).save()
182
- query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first()
183
- channel_name = query_result_3.channel_name if query_result_3 else ""
184
- channel_layer = get_channel_layer()
185
- async_to_sync(channel_layer.send)(channel_name, {
186
- 'type': 'event_send',
187
- 'data': {
188
- "type": "chapter_ready_to_download",
189
- "data": {
190
- "source": source,
191
- "comic_id": comic_id,
192
- "chapter_id": chapter_id,
193
- "chapter_idx": chapter_idx
194
- }
195
- }
196
- })
197
- SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete()
198
-
 
 
 
 
 
 
 
 
 
199
  connections['cache'].close()
200
  else:
201
  connections['cache'].close()
@@ -206,18 +237,24 @@ class Job(Thread):
206
  if os.path.exists(input_dir): shutil.rmtree(input_dir)
207
  if (managed_output_dir):
208
  if os.path.exists(managed_output_dir): shutil.rmtree(managed_output_dir)
 
 
209
  query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first()
210
  channel_name = query_result_3.channel_name if query_result_3 else ""
211
- channel_layer = get_channel_layer()
212
- async_to_sync(channel_layer.send)(channel_name, {
213
- 'type': 'event_send',
214
- 'data': {
215
- "type": "chapter_ready_to_download",
216
- "data": {"state":"error"}
217
- }
218
- })
219
-
220
  SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete()
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  connections['cache'].close()
222
  sleep(10)
223
 
 
2
  from time import sleep
3
  from backend.module.utils import date_utils
4
  from django.db import connections
5
+ from backend.models.model_cache import SocketRequestChapterQueueCache
6
+ from backend.models.model_1 import ComicStorageCache
7
  from core.settings import BASE_DIR
8
  from backend.module import web_scrap
9
  from backend.module.utils import manage_image
 
23
 
24
  STORAGE_DIR = os.path.join(BASE_DIR,"storage")
25
  if not os.path.exists(STORAGE_DIR): os.makedirs(STORAGE_DIR)
26
+ COMIC_STORAGE_DIR = os.path.join(STORAGE_DIR,"comics")
27
+ if not os.path.exists(COMIC_STORAGE_DIR): os.makedirs(COMIC_STORAGE_DIR)
28
 
29
  LOG_DIR = os.path.join(BASE_DIR, "log")
30
  if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR)
 
40
 
41
  query_result = SocketRequestChapterQueueCache.objects.order_by("datetime").first()
42
  while True:
43
+ if (GetDirectorySize(COMIC_STORAGE_DIR) >= MAX_STORAGE_SIZE):
44
  query_result_2 = ComicStorageCache.objects.order_by("datetime").first()
45
  if (query_result_2):
46
  file_path = query_result_2.file_path
 
76
  else:
77
  connections['cache'].close()
78
 
79
+ if (options.get("colorize") or options.get("translate").get("state")):
80
+ script = []
81
+ input_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"temp")
82
+ merge_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"merged")
83
 
84
+ if (options.get("translate").get("state") and options.get("colorize")):
85
+ managed_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated_colorized")
86
+ script = [
87
+ "python", "-m", "manga_translator", "-v", "--overwrite", "--attempts=3", "--ocr=mocr", "--no-text-lang-skip", "--det-auto-rotate", "--det-gamma-correct", "--colorize=mc2", "--translator=m2m100_big",
88
+ "-l", f"{options.get("translate").get("target")}", "-i", f"{merge_output_dir}", "-o", f"{managed_output_dir}"
89
+ ]
90
+ elif (options.get("translate").get("state") and not options.get("colorize")):
91
+ managed_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated")
92
+ script = [
93
+ "python", "-m", "manga_translator", "-v", "--overwrite", "--attempts=3", "--ocr=mocr", "--no-text-lang-skip", "--det-auto-rotate", "--det-gamma-correct", "--translator=m2m100_big",
94
+ "-l", f"{options.get("translate").get("target")}", "-i", f"{merge_output_dir}", "-o", f"{managed_output_dir}"
95
+ ]
96
+ elif (options.get("colorize") and not options.get("translate").get("state")):
97
+
98
+ managed_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"colorized")
99
+ script = [
100
+ "python", "-m", "manga_translator", "-v", "--overwrite", "--attempts=3", "--detector=none", "--translator=original", "--colorize=mc2", "--colorization-size=-1",
101
+ "-i", f"{merge_output_dir}", "-o", f"{managed_output_dir}"
102
+ ]
103
 
104
+ if target_lang == "ENG": script.append("--manga2eng")
105
 
106
 
 
 
107
  if os.path.exists(input_dir): shutil.rmtree(input_dir)
108
+ if os.path.exists(merge_output_dir): shutil.rmtree(merge_output_dir)
109
  if os.path.exists(managed_output_dir): shutil.rmtree(managed_output_dir)
110
 
111
  job = web_scrap.source_control[source].get_chapter.scrap(comic_id=comic_id,chapter_id=chapter_id,output_dir=input_dir)
112
+
113
  if job.get("status") == "success":
114
+ manage_image.merge_images_vertically(input_dir, merge_output_dir, max_height=1800)
115
+ shutil.rmtree(input_dir)
116
+
117
 
118
  with open(os.path.join(LOG_DIR,"image_translator_output.log"), "w") as file:
119
  result = subprocess.run(
 
127
  )
128
  if result.returncode != 0: raise Exception("Image Translator Execution error!")
129
  os.makedirs(managed_output_dir,exist_ok=True)
130
+ shutil.rmtree(merge_output_dir)
131
 
132
  with zipfile.ZipFile(managed_output_dir + '.zip', 'w') as zipf:
133
  for foldername, subfolders, filenames in os.walk(managed_output_dir):
 
153
 
154
  query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first()
155
  channel_name = query_result_3.channel_name if query_result_3 else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete()
157
+ if channel_name:
158
+ channel_layer = get_channel_layer()
159
+ async_to_sync(channel_layer.send)(channel_name, {
160
+ 'type': 'event_send',
161
+ 'data': {
162
+ "type": "chapter_ready_to_download",
163
+ "data": {
164
+ "source": source,
165
+ "comic_id": comic_id,
166
+ "chapter_id": chapter_id,
167
+ "chapter_idx": chapter_idx
168
+ }
169
+ }
170
+ })
171
+ connections['cache'].close()
172
+
173
+ else:
174
+ connections['cache'].close()
175
+ raise Exception("#1 Dowload chapter error!")
176
  else:
177
+ input_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"original")
178
+ merge_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"origin-merged")
179
+
180
  if os.path.exists(input_dir): shutil.rmtree(input_dir)
181
+ if os.path.exists(merge_output_dir): shutil.rmtree(merge_output_dir)
182
 
183
  job = web_scrap.source_control["colamanga"].get_chapter.scrap(comic_id=comic_id,chapter_id=chapter_id,output_dir=input_dir)
184
+ if job.get("status") == "success":
185
+ manage_image.merge_images_vertically(input_dir, merge_output_dir, max_height=1800)
186
+ if os.path.exists(input_dir): shutil.rmtree(input_dir)
187
+
188
+ with zipfile.ZipFile(input_dir + '.zip', 'w') as zipf:
189
+ for foldername, subfolders, filenames in os.walk(merge_output_dir):
190
+ for filename in filenames:
191
+ if filename.endswith(('.png', '.jpg', '.jpeg')):
192
+ file_path = os.path.join(foldername, filename)
193
+ zipf.write(file_path, os.path.basename(file_path))
194
+ shutil.rmtree(merge_output_dir)
195
+
196
+ ComicStorageCache(
197
+ source = source,
198
+ comic_id = comic_id,
199
+ chapter_id = chapter_id,
200
+ chapter_idx = chapter_idx,
201
+ file_path = input_dir + '.zip',
202
+ colorize = False,
203
+ translate = False,
204
+ target_lang = "",
205
+
206
+ ).save()
207
+
208
+
209
+ query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first()
210
+ channel_name = query_result_3.channel_name if query_result_3 else ""
211
+ SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete()
212
+
213
+ if channel_name:
214
+ channel_layer = get_channel_layer()
215
+ async_to_sync(channel_layer.send)(channel_name, {
216
+ 'type': 'event_send',
217
+ 'data': {
218
+ "type": "chapter_ready_to_download",
219
+ "data": {
220
+ "source": source,
221
+ "comic_id": comic_id,
222
+ "chapter_id": chapter_id,
223
+ "chapter_idx": chapter_idx
224
+ }
225
+ }
226
+ })
227
+ else:
228
+ connections['cache'].close()
229
+ raise Exception("#2 Dowload chapter error!")
230
  connections['cache'].close()
231
  else:
232
  connections['cache'].close()
 
237
  if os.path.exists(input_dir): shutil.rmtree(input_dir)
238
  if (managed_output_dir):
239
  if os.path.exists(managed_output_dir): shutil.rmtree(managed_output_dir)
240
+
241
+
242
  query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first()
243
  channel_name = query_result_3.channel_name if query_result_3 else ""
 
 
 
 
 
 
 
 
 
244
  SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete()
245
+
246
+ if (channel_name):
247
+ channel_layer = get_channel_layer()
248
+ async_to_sync(channel_layer.send)(channel_name, {
249
+ 'type': 'event_send',
250
+ 'data': {
251
+ "type": "chapter_ready_to_download",
252
+ "data": {"state":"error"}
253
+ }
254
+ })
255
+
256
+
257
+
258
  connections['cache'].close()
259
  sleep(10)
260
 
backend/migrations/0001_initial.py CHANGED
@@ -1,5 +1,6 @@
1
- # Generated by Django 5.1.1 on 2024-10-01 06:35
2
 
 
3
  import backend.models.model_cache
4
  import uuid
5
  from django.db import migrations, models
@@ -32,13 +33,12 @@ class Migration(migrations.Migration):
32
  ('colorize', models.BooleanField()),
33
  ('translate', models.BooleanField()),
34
  ('target_lang', models.TextField()),
35
- ('datetime', models.DateTimeField(default=backend.models.model_cache.get_current_utc_time)),
36
  ],
37
  ),
38
  migrations.CreateModel(
39
  name='RequestCache',
40
  fields=[
41
- ('room', models.TextField()),
42
  ('client', models.UUIDField(primary_key=True, serialize=False)),
43
  ('datetime', models.DateTimeField(default=backend.models.model_cache.get_current_utc_time)),
44
  ],
 
1
+ # Generated by Django 5.1.1 on 2024-12-01 11:29
2
 
3
+ import backend.models.model_1
4
  import backend.models.model_cache
5
  import uuid
6
  from django.db import migrations, models
 
33
  ('colorize', models.BooleanField()),
34
  ('translate', models.BooleanField()),
35
  ('target_lang', models.TextField()),
36
+ ('datetime', models.DateTimeField(default=backend.models.model_1.get_current_utc_time)),
37
  ],
38
  ),
39
  migrations.CreateModel(
40
  name='RequestCache',
41
  fields=[
 
42
  ('client', models.UUIDField(primary_key=True, serialize=False)),
43
  ('datetime', models.DateTimeField(default=backend.models.model_cache.get_current_utc_time)),
44
  ],
backend/migrations/0002_remove_requestcache_room.py DELETED
@@ -1,17 +0,0 @@
1
- # Generated by Django 5.1.1 on 2024-11-08 17:33
2
-
3
- from django.db import migrations
4
-
5
-
6
- class Migration(migrations.Migration):
7
-
8
- dependencies = [
9
- ('backend', '0001_initial'),
10
- ]
11
-
12
- operations = [
13
- migrations.RemoveField(
14
- model_name='requestcache',
15
- name='room',
16
- ),
17
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/migrations/0002_webscrapegetcovercache.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.1.1 on 2024-12-01 11:38
2
+
3
+ import backend.models.model_1
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('backend', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='WebscrapeGetCoverCache',
16
+ fields=[
17
+ ('file_path', models.TextField(primary_key=True, serialize=False)),
18
+ ('source', models.TextField()),
19
+ ('comic_id', models.TextField()),
20
+ ('cover_id', models.TextField()),
21
+ ('datetime', models.DateTimeField(default=backend.models.model_1.get_current_utc_time)),
22
+ ],
23
+ ),
24
+ ]
backend/migrations/__pycache__/0001_initial.cpython-312.pyc CHANGED
Binary files a/backend/migrations/__pycache__/0001_initial.cpython-312.pyc and b/backend/migrations/__pycache__/0001_initial.cpython-312.pyc differ
 
backend/migrations/__pycache__/0002_remove_requestcache_room.cpython-312.pyc DELETED
Binary file (616 Bytes)
 
backend/migrations/__pycache__/0002_webscrapegetcovercache.cpython-312.pyc ADDED
Binary file (1.2 kB). View file
 
backend/models/__pycache__/model_1.cpython-312.pyc CHANGED
Binary files a/backend/models/__pycache__/model_1.cpython-312.pyc and b/backend/models/__pycache__/model_1.cpython-312.pyc differ
 
backend/models/__pycache__/model_cache.cpython-312.pyc CHANGED
Binary files a/backend/models/__pycache__/model_cache.cpython-312.pyc and b/backend/models/__pycache__/model_cache.cpython-312.pyc differ
 
backend/models/model_1.py CHANGED
@@ -1,8 +1,28 @@
1
  from django.db import models
2
- # Create your models here.
 
3
 
 
4
 
5
- # class AdminAccount(models.Model):
6
- # account = models.CharField(max_length=36,default="")
7
- # password = models.CharField(max_length=36,default="")
 
 
 
 
 
 
 
 
 
 
 
 
8
 
 
 
 
 
 
 
 
1
  from django.db import models
2
+ from backend.module.utils import date_utils
3
+ from core.settings import BASE_DIR
4
 
5
+ import uuid, os
6
 
7
+ def get_current_utc_time(): return date_utils.utc_time().get()
8
+
9
+
10
+
11
+ class ComicStorageCache(models.Model):
12
+ id = models.UUIDField(primary_key = True, default = uuid.uuid4, editable = False)
13
+ source = models.TextField()
14
+ comic_id = models.TextField()
15
+ chapter_id = models.TextField()
16
+ chapter_idx = models.IntegerField()
17
+ file_path = models.TextField()
18
+ colorize = models.BooleanField()
19
+ translate = models.BooleanField()
20
+ target_lang = models.TextField()
21
+ datetime = models.DateTimeField(default=get_current_utc_time)
22
 
23
+ class WebscrapeGetCoverCache(models.Model):
24
+ file_path = models.TextField(primary_key=True)
25
+ source = models.TextField()
26
+ comic_id = models.TextField()
27
+ cover_id = models.TextField()
28
+ datetime = models.DateTimeField(default=get_current_utc_time)
backend/models/model_cache.py CHANGED
@@ -12,18 +12,6 @@ class CloudflareTurnStileCache(models.Model):
12
  token = models.TextField(primary_key=True)
13
  datetime = models.DateTimeField(default=get_current_utc_time)
14
 
15
- class ComicStorageCache(models.Model):
16
- id = models.UUIDField(primary_key = True, default = uuid.uuid4, editable = False)
17
- source = models.TextField()
18
- comic_id = models.TextField()
19
- chapter_id = models.TextField()
20
- chapter_idx = models.IntegerField()
21
- file_path = models.TextField()
22
- colorize = models.BooleanField()
23
- translate = models.BooleanField()
24
- target_lang = models.TextField()
25
- datetime = models.DateTimeField(default=get_current_utc_time)
26
-
27
 
28
  class SocketRequestChapterQueueCache(models.Model):
29
  id = models.UUIDField(primary_key = True, default = uuid.uuid4, editable = False)
 
12
  token = models.TextField(primary_key=True)
13
  datetime = models.DateTimeField(default=get_current_utc_time)
14
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  class SocketRequestChapterQueueCache(models.Model):
17
  id = models.UUIDField(primary_key = True, default = uuid.uuid4, editable = False)
backend/module/utils/__pycache__/manage_image.cpython-312.pyc CHANGED
Binary files a/backend/module/utils/__pycache__/manage_image.cpython-312.pyc and b/backend/module/utils/__pycache__/manage_image.cpython-312.pyc differ
 
backend/module/utils/manage_image.py CHANGED
@@ -1,7 +1,7 @@
1
  import os
2
  from PIL import Image
3
 
4
- def merge_images_vertically(input_dir, output_dir, max_size=10 * 1024 * 1024):
5
  os.makedirs(output_dir,exist_ok=True)
6
 
7
  filenames = sorted(os.listdir(input_dir), key=lambda x: int(x.split(".")[0]))
@@ -58,6 +58,57 @@ def merge_images_vertically(input_dir, output_dir, max_size=10 * 1024 * 1024):
58
  merged_image.save(os.path.join(output_dir, f"{merged_file_index}.png"))
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  def split_image_vertically(input_dir, output_dir):
62
  os.makedirs(output_dir,exist_ok=True)
63
 
 
1
  import os
2
  from PIL import Image
3
 
4
+ def merge_images_vertically_old(input_dir, output_dir, max_size=1800):
5
  os.makedirs(output_dir,exist_ok=True)
6
 
7
  filenames = sorted(os.listdir(input_dir), key=lambda x: int(x.split(".")[0]))
 
58
  merged_image.save(os.path.join(output_dir, f"{merged_file_index}.png"))
59
 
60
 
61
+ def merge_images_vertically(input_dir, output_dir, max_height=1800):
62
+ os.makedirs(output_dir, exist_ok=True)
63
+
64
+ filenames = sorted(os.listdir(input_dir), key=lambda x: int(x.split(".")[0]))
65
+
66
+ merged_image = None
67
+ merged_file_index = 0
68
+
69
+ index = 0
70
+ while True:
71
+ if index > (len(filenames) - 1): break
72
+ file = filenames[index]
73
+ if not merged_image:
74
+ image = Image.open(os.path.join(input_dir, file))
75
+
76
+ width, height = image.size
77
+
78
+ new_image = Image.new("RGBA", (width, height))
79
+
80
+ # Paste the image onto the new image
81
+ new_image.paste(image, (0, 0))
82
+ merged_image = new_image
83
+ index += 1
84
+ else:
85
+ merged_width, merged_height = merged_image.size
86
+
87
+ if merged_height >= max_height:
88
+ output_path = os.path.join(output_dir, f"{merged_file_index}.png")
89
+ merged_image.save(output_path)
90
+ merged_image = None
91
+ merged_file_index += 1
92
+ else:
93
+ image = Image.open(os.path.join(input_dir, file))
94
+ width, height = image.size
95
+
96
+ # Create a new image with the combined width and the height of the tallest image
97
+ new_width = max(merged_width, width)
98
+ new_height = merged_height + height
99
+ new_image = Image.new("RGB", (new_width, new_height))
100
+
101
+ # Paste the two images onto the new image
102
+ new_image.paste(merged_image, (0, 0))
103
+ new_image.paste(image, (0, merged_height))
104
+ merged_image = new_image
105
+ index += 1
106
+
107
+ if merged_image:
108
+ output_path = os.path.join(output_dir, f"{merged_file_index}.png")
109
+ merged_image.save(output_path)
110
+
111
+
112
  def split_image_vertically(input_dir, output_dir):
113
  os.makedirs(output_dir,exist_ok=True)
114
 
backend/module/web_scrap/__pycache__/utils.cpython-312.pyc CHANGED
Binary files a/backend/module/web_scrap/__pycache__/utils.cpython-312.pyc and b/backend/module/web_scrap/__pycache__/utils.cpython-312.pyc differ
 
backend/module/web_scrap/utils.py CHANGED
@@ -17,7 +17,6 @@ class SeleniumScraper:
17
  options.add_argument('--no-sandbox')
18
  options.add_argument("--no-quit")
19
  options.add_argument('--disable-extensions')
20
- options.add_argument('--disable-gpu')
21
  options.add_argument('--disable-dev-shm-usage')
22
  options.set_capability('goog:loggingPrefs', {'performance': 'ALL'})
23
  self.__driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
 
17
  options.add_argument('--no-sandbox')
18
  options.add_argument("--no-quit")
19
  options.add_argument('--disable-extensions')
 
20
  options.add_argument('--disable-dev-shm-usage')
21
  options.set_capability('goog:loggingPrefs', {'performance': 'ALL'})
22
  self.__driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
backend/urls.py CHANGED
@@ -1,7 +1,7 @@
1
 
2
  from django.contrib import admin
3
  from django.urls import path, include, re_path
4
- from backend.api import test, stream_file, web_scrap, cloudflare_turnstile, queue
5
 
6
 
7
 
@@ -14,10 +14,9 @@ urlpatterns = [
14
  # /api/queue/request_info/
15
  path("cloudflare_turnstile/verify/", cloudflare_turnstile.verify),
16
 
17
- path('web_scrap/get_list/', web_scrap.get_list),
18
- path('web_scrap/search/', web_scrap.search),
19
- path('web_scrap/get/', web_scrap.get),
20
- path('web_scrap/get_cover/<str:source>/<str:id>/<str:cover_id>/', web_scrap.get_cover),
21
 
22
 
23
 
 
1
 
2
  from django.contrib import admin
3
  from django.urls import path, include, re_path
4
+ from backend.api import test, stream_file, cloudflare_turnstile, queue, web_scrape
5
 
6
 
7
 
 
14
  # /api/queue/request_info/
15
  path("cloudflare_turnstile/verify/", cloudflare_turnstile.verify),
16
 
17
+ path('web_scrap/get_list/', web_scrape.get_list),
18
+ path('web_scrap/get/', web_scrape.get),
19
+ path('web_scrap/get_cover/<str:source>/<str:id>/<str:cover_id>/', web_scrape.get_cover),
 
20
 
21
 
22
 
core/__pycache__/settings.cpython-312.pyc CHANGED
Binary files a/core/__pycache__/settings.cpython-312.pyc and b/core/__pycache__/settings.cpython-312.pyc differ
 
core/settings.py CHANGED
@@ -21,8 +21,7 @@ for key, value in os.environ.items():
21
 
22
 
23
  # SECURITY WARNING: don't run with debug turned on in production!
24
- DEBUG = True
25
- DEBUG_DB = True
26
 
27
  # settings.py
28
 
@@ -117,37 +116,27 @@ WSGI_APPLICATION = 'core.wsgi.application'
117
  # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
118
  DATABASE_ROUTERS = ['core.routers.Router']
119
 
120
- if DEBUG or DEBUG_DB:
121
- DATABASES = {
122
- 'default': {
123
- 'ENGINE': 'django.db.backends.sqlite3',
124
- 'NAME': BASE_DIR / 'database.sqlite3',
125
- },
126
- 'cache': {
127
- 'ENGINE': 'django.db.backends.sqlite3',
128
- 'NAME': BASE_DIR / 'cache.sqlite3',
129
- },
130
- 'DB1': {
131
- 'ENGINE': 'django.db.backends.sqlite3',
132
- 'NAME': BASE_DIR / 'db1.sqlite3',
133
- },
134
- 'DB2': {
135
- 'ENGINE': 'django.db.backends.sqlite3',
136
- 'NAME': BASE_DIR / 'db2.sqlite3',
137
- },
138
 
139
- }
140
- else:
141
- DATABASES = {
142
- 'default': dj_database_url.config(default=env("DB")),
143
- 'cache': {
144
- 'ENGINE': 'django.db.backends.sqlite3',
145
- 'NAME': BASE_DIR / 'cache.sqlite3',
146
- },
147
- 'DB1': dj_database_url.config(default=env("DB1")),
148
- 'DB2': dj_database_url.config(default=env("DB2")),
149
-
150
- }
 
 
 
 
 
 
 
 
151
 
152
 
153
 
 
21
 
22
 
23
  # SECURITY WARNING: don't run with debug turned on in production!
24
+ DEBUG = False
 
25
 
26
  # settings.py
27
 
 
116
  # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
117
  DATABASE_ROUTERS = ['core.routers.Router']
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ DATABASES = {
121
+ 'default': {
122
+ 'ENGINE': 'django.db.backends.sqlite3',
123
+ 'NAME': BASE_DIR / 'database.sqlite3',
124
+ },
125
+ 'cache': {
126
+ 'ENGINE': 'django.db.backends.sqlite3',
127
+ 'NAME': BASE_DIR / 'cache.sqlite3',
128
+ },
129
+ 'DB1': {
130
+ 'ENGINE': 'django.db.backends.sqlite3',
131
+ 'NAME': BASE_DIR / 'db1.sqlite3',
132
+ },
133
+ 'DB2': {
134
+ 'ENGINE': 'django.db.backends.sqlite3',
135
+ 'NAME': BASE_DIR / 'db2.sqlite3',
136
+ },
137
+
138
+ }
139
+
140
 
141
 
142
 
frontend/app/_layout.tsx CHANGED
@@ -3,7 +3,7 @@ import { useFonts } from 'expo-font';
3
  import { Stack, router, usePathname } from 'expo-router';
4
  import * as SplashScreen from 'expo-splash-screen';
5
  import { useEffect, useState, useContext, createContext, memo } from 'react';
6
- import { useWindowDimensions, View, Text, Pressable } from 'react-native';
7
  import 'react-native-reanimated';
8
  import { SafeAreaView } from 'react-native-safe-area-context';
9
  import Menu from '@/components/menu/menu';
@@ -134,7 +134,15 @@ return (<>{loaded && themeTypeContext && apiBaseContext && socketBaseContext &&
134
  widgetContext, setWidgetContext,
135
  showCloudflareTurnstileContext, setShowCloudflareTurnstileContext,
136
  }}>
137
- <View style={{width:Dimensions.width,height:Dimensions.height,backgroundColor: Theme[themeTypeContext].background_color}}>
 
 
 
 
 
 
 
 
138
  {showCloudflareTurnstileContext
139
  ? <View style={{position:"absolute",width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",backgroundColor:Theme[themeTypeContext].background_color}}>
140
  <CloudflareTurnstile
@@ -184,7 +192,7 @@ return (<>{loaded && themeTypeContext && apiBaseContext && socketBaseContext &&
184
  }
185
 
186
 
187
- </View>
188
  <Toast config={toastConfig} />
189
  </CONTEXT.Provider>
190
  </SafeAreaView>
 
3
  import { Stack, router, usePathname } from 'expo-router';
4
  import * as SplashScreen from 'expo-splash-screen';
5
  import { useEffect, useState, useContext, createContext, memo } from 'react';
6
+ import { useWindowDimensions, View, Text, Pressable, KeyboardAvoidingView } from 'react-native';
7
  import 'react-native-reanimated';
8
  import { SafeAreaView } from 'react-native-safe-area-context';
9
  import Menu from '@/components/menu/menu';
 
134
  widgetContext, setWidgetContext,
135
  showCloudflareTurnstileContext, setShowCloudflareTurnstileContext,
136
  }}>
137
+ <KeyboardAvoidingView
138
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
139
+ style={{
140
+ width:Dimensions.width,
141
+ height:Dimensions.height,
142
+
143
+ backgroundColor: Theme[themeTypeContext].background_color
144
+ }}
145
+ >
146
  {showCloudflareTurnstileContext
147
  ? <View style={{position:"absolute",width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",backgroundColor:Theme[themeTypeContext].background_color}}>
148
  <CloudflareTurnstile
 
192
  }
193
 
194
 
195
+ </KeyboardAvoidingView>
196
  <Toast config={toastConfig} />
197
  </CONTEXT.Provider>
198
  </SafeAreaView>
frontend/app/bookmark/_layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Tabs, Stack } from 'expo-router';
2
+ import React from 'react';
3
+ import {View, Text} from 'react-native';
4
+
5
+ import { Colors } from '@/constants/Colors';
6
+ import { useColorScheme } from '@/hooks/useColorScheme';
7
+ import { SafeAreaView } from 'react-native-safe-area-context';
8
+ export default function TabLayout() {
9
+ const colorScheme = useColorScheme();
10
+
11
+ return (
12
+ <Stack
13
+ screenOptions={{
14
+ headerShown: false,
15
+ }}>
16
+
17
+ </Stack>
18
+ );
19
+ }
frontend/app/bookmark/components/bookmark_component.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react';
2
+ import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router';
3
+ import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper';
6
+ import CircularProgress from 'react-native-circular-progress-indicator';
7
+ import { ActivityIndicator } from 'react-native-paper';
8
+ import { FlashList } from "@shopify/flash-list";
9
+
10
+
11
+ import uuid from 'react-native-uuid';
12
+ import Toast from 'react-native-toast-message';
13
+ import { View, AnimatePresence } from 'moti';
14
+ import * as Clipboard from 'expo-clipboard';
15
+ import * as FileSystem from 'expo-file-system';
16
+ import NetInfo from "@react-native-community/netinfo";
17
+ import { Marquee } from '@animatereactnative/marquee';
18
+ import { Slider } from '@rneui/themed-edge';
19
+
20
+ import { __styles } from '../stylesheet/styles';
21
+ import Storage from '@/constants/module/storages/storage';
22
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
23
+ import Image from '@/components/Image';
24
+ import {CONTEXT} from '@/constants/module/context';
25
+ import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager";
26
+ import Theme from '@/constants/theme';
27
+ import ComicStorage from '@/constants/module/storages/comic_storage';
28
+
29
+ const BookmarkComponent = ({item, SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK}:any) => {
30
+ const Dimensions = useWindowDimensions();
31
+ const controller = new AbortController();
32
+ const signal = controller.signal;
33
+
34
+ const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
35
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
36
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
37
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
38
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
39
+
40
+
41
+ useFocusEffect(useCallback(() => {
42
+
43
+
44
+ return () => {
45
+ controller.abort();
46
+ };
47
+ },[]))
48
+
49
+ return (<>
50
+ <TouchableRipple
51
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
52
+ style={{
53
+ width:"auto",
54
+ height:"auto",
55
+ backgroundColor: SELECTED_BOOKMARK === item ? Theme[themeTypeContext].border_color : "transparent",
56
+ borderBottomWidth: SELECTED_BOOKMARK === item ? 3.5 : 0,
57
+ borderColor:Theme[themeTypeContext].button_color,
58
+ paddingHorizontal:18,
59
+ paddingVertical:14,
60
+ borderRadius:2,
61
+ }}
62
+ onPress={()=>{SET_SELECTED_BOOKMARK(item)}}
63
+ >
64
+ <View
65
+ style={{
66
+ width:"auto",
67
+ height:"auto",
68
+ }}
69
+ >
70
+ <Text selectable={false}
71
+ style={{
72
+ color:Theme[themeTypeContext].text_color,
73
+ fontFamily:"roboto-bold",
74
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
75
+ }}
76
+ >
77
+ {item}
78
+ </Text>
79
+ </View>
80
+ </TouchableRipple>
81
+ </>)
82
+
83
+
84
+ }
85
+
86
+ export default BookmarkComponent;
87
+
frontend/app/bookmark/components/comic_component.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react';
2
+ import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router';
3
+ import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper';
6
+ import CircularProgress from 'react-native-circular-progress-indicator';
7
+ import { ActivityIndicator } from 'react-native-paper';
8
+ import { FlashList } from "@shopify/flash-list";
9
+
10
+
11
+ import uuid from 'react-native-uuid';
12
+ import Toast from 'react-native-toast-message';
13
+ import { View, AnimatePresence } from 'moti';
14
+ import * as Clipboard from 'expo-clipboard';
15
+ import * as FileSystem from 'expo-file-system';
16
+ import NetInfo from "@react-native-community/netinfo";
17
+ import { Marquee } from '@animatereactnative/marquee';
18
+ import { Slider } from '@rneui/themed-edge';
19
+
20
+ import { __styles } from '../stylesheet/styles';
21
+ import Storage from '@/constants/module/storages/storage';
22
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
23
+ import CoverStorage from '@/constants/module/storages/cover_storage';
24
+
25
+ import Image from '@/components/Image';
26
+ import {CONTEXT} from '@/constants/module/context';
27
+ import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager";
28
+ import Theme from '@/constants/theme';
29
+ import ComicStorage from '@/constants/module/storages/comic_storage';
30
+
31
+ const ComicComponent = ({item, SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK}:any) => {
32
+ const Dimensions = useWindowDimensions();
33
+ const controller = new AbortController();
34
+ const signal = controller.signal;
35
+
36
+ const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
37
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
38
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
39
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
40
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
41
+
42
+ const [styles, setStyles]:any = useState(null)
43
+ const [isLoading, setIsLoading] = useState<boolean>(true)
44
+
45
+ const cover:any = useRef("")
46
+
47
+ useFocusEffect(useCallback(() => {
48
+ (async ()=>{
49
+ setIsLoading(true)
50
+ setStyles(__styles(themeTypeContext,Dimensions))
51
+ const stored_bookmark = await Storage.get("bookmark") || []
52
+ console.log(stored_bookmark)
53
+ cover.current = await CoverStorage.get(`${item.source}-${item.id}`) || ""
54
+ setIsLoading(false)
55
+ })()
56
+
57
+ return () => {
58
+ cover.current = ""
59
+ controller.abort();
60
+ };
61
+ },[]))
62
+
63
+ return (<>{styles && !isLoading && <>
64
+ <TouchableRipple
65
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
66
+ onPress={()=>{router.navigate(`/view/${item.source}/${item.id}/?mode=local`)}}
67
+ style={styles.item_box}
68
+ >
69
+ <>
70
+ <Image onError={(error:any)=>{console.log("load image error",error)}} source={cover.current}
71
+ style={styles.item_cover}
72
+ contentFit="cover" transition={1000}
73
+ onLoadEnd={()=>{cover.current = ""}}
74
+ />
75
+
76
+ <Text style={styles.item_title}>{item.info.title}</Text>
77
+ </>
78
+ </TouchableRipple>
79
+ </>}</>)
80
+
81
+
82
+ }
83
+
84
+ export default ComicComponent;
85
+
frontend/app/bookmark/components/widgets/bookmark.tsx ADDED
@@ -0,0 +1,904 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useEffect, useState, useCallback, useContext, useRef, Fragment, memo } from 'react';
3
+ import { Platform, useWindowDimensions, ScrollView } from 'react-native';
4
+
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper';
6
+ import { View, AnimatePresence } from 'moti';
7
+ import Toast from 'react-native-toast-message';
8
+ import * as FileSystem from 'expo-file-system';
9
+ import axios from 'axios';
10
+
11
+
12
+ import Theme from '@/constants/theme';
13
+ import Dropdown from '@/components/dropdown';
14
+ import { CONTEXT } from '@/constants/module/context';
15
+ import Storage from '@/constants/module/storages/storage';
16
+ import ComicStorage from '@/constants/module/storages/comic_storage';
17
+ import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
18
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
19
+ import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage';
20
+
21
+ interface BookmarkWidgetProps {
22
+ setIsLoading: any;
23
+ onRefresh: any;
24
+ }
25
+
26
+ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
27
+ setIsLoading,
28
+ onRefresh,
29
+ }) => {
30
+ const Dimensions = useWindowDimensions();
31
+
32
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
33
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
34
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
35
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
36
+
37
+ const [BOOKMARK_DATA, SET_BOOKMARK_DATA]: any = useState([])
38
+ const [MIGRATE_BOOKMARK_DATA, SET_MIGRATE_BOOKMARK_DATA]:any = useState([])
39
+
40
+
41
+ const [showMenuOption, setShowMenuOption]:any = useState({state:false,positions:[0,0,0,0],id:""})
42
+ const [searchTag, setSearchTag]:any = useState("")
43
+
44
+ const [migrateTag,setMigrateTag]:any = useState("")
45
+
46
+ const [manageBookmark, setManageBookmark]:any = useState({edit:"",delete:""})
47
+ const [createTag, setCreateTag]:any = useState({state:false,title:""})
48
+ const [removeTag, setRemoveTag]:any = useState({state:false, removing: false})
49
+
50
+
51
+ const controller = new AbortController();
52
+ const signal = controller.signal;
53
+
54
+ const RenderTag = useCallback(({item}:any) =>{
55
+ const [editTag, setEditTag]:any = useState(item.value)
56
+ useEffect(()=>{
57
+ },[manageBookmark])
58
+
59
+ return (<>
60
+ {item.value.includes(searchTag) &&
61
+ (
62
+ <View
63
+ style={{
64
+ display:"flex",
65
+ flexDirection:"row",
66
+ alignItems:"center",
67
+ justifyContent:"space-between",
68
+ gap:8,
69
+ zIndex:10,
70
+ }}
71
+ >
72
+ <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value &&
73
+ (<View
74
+ style={{
75
+ width:"100%",
76
+ display:"flex",
77
+ flexDirection:"row",
78
+ justifyContent:"space-between",
79
+ alignItems:"center",
80
+ height:"auto",
81
+ gap:18,
82
+ }}
83
+ >
84
+ <Text
85
+ style={{
86
+ color:"white",
87
+ fontFamily:"roboto-medium",
88
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.025
89
+ }}
90
+ >{item.label}</Text>
91
+ <View
92
+ style={{
93
+ width:"auto",
94
+ height:"auto",
95
+
96
+ }}
97
+ >
98
+
99
+ <TouchableRipple
100
+
101
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
102
+ style={{
103
+ borderRadius:5,
104
+ borderWidth:0,
105
+ backgroundColor: "transparent",
106
+ padding:5,
107
+
108
+ }}
109
+
110
+ onPress={(event)=>{
111
+ if (manageBookmark.edit){
112
+ setManageBookmark({...manageBookmark,edit:""})
113
+ setEditTag("")
114
+ }
115
+
116
+ const x = event.nativeEvent.pageX
117
+ const y = event.nativeEvent.pageY
118
+
119
+ setShowMenuOption({
120
+ ...showMenuOption,
121
+ state: showMenuOption.id === item.value ? false : true,
122
+ positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0],
123
+ id:showMenuOption.id === item.value ? "" : item.value,
124
+ })
125
+
126
+
127
+
128
+ }}
129
+ >
130
+
131
+ <Icon source={"dots-vertical"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={Theme[themeTypeContext].icon_color}/>
132
+ </TouchableRipple>
133
+ </View>
134
+ </View>)
135
+ }</>
136
+ <>{manageBookmark.edit === item.value &&
137
+ (<View
138
+ style={{
139
+ display:"flex",
140
+ flexDirection:"row",
141
+ justifyContent:"space-between",
142
+ alignItems:"center",
143
+ width:"100%",
144
+ height:"auto",
145
+ gap:12,
146
+ padding:12,
147
+ }}
148
+ >
149
+ <View
150
+ style={{flex:1}}
151
+ >
152
+ <TextInput mode="outlined" label="Edit" textColor={Theme[themeTypeContext].text_color} maxLength={72}
153
+ autoFocus={true}
154
+ right={<TextInput.Affix text={`| Max: 72`} />}
155
+ style={{
156
+ backgroundColor:Theme[themeTypeContext].background_color,
157
+ borderColor:Theme[themeTypeContext].border_color,
158
+
159
+ }}
160
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
161
+ value={editTag}
162
+ onChangeText={(text)=>{
163
+ setEditTag(text)
164
+ }}
165
+ />
166
+ </View>
167
+ <View
168
+ style={{
169
+ display:"flex",
170
+ flexDirection:"row",
171
+ gap:8,
172
+ }}
173
+ >
174
+ <TouchableRipple
175
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
176
+ style={{
177
+ borderRadius:5,
178
+ borderWidth:0,
179
+ backgroundColor: "transparent",
180
+ padding:5,
181
+ }}
182
+
183
+ onPress={()=>{
184
+ setManageBookmark({...manageBookmark,edit:""})
185
+ setEditTag("")
186
+ setShowMenuOption({...showMenuOption,state:false,id:""})
187
+ }}
188
+ >
189
+
190
+ <Icon source={"close"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"red"}/>
191
+ </TouchableRipple>
192
+ <TouchableRipple
193
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
194
+ style={{
195
+ borderRadius:5,
196
+ borderWidth:0,
197
+ backgroundColor: "transparent",
198
+ padding:5,
199
+ }}
200
+
201
+ onPress={async ()=>{
202
+ const stored_bookmark = await Storage.get("bookmark");
203
+
204
+ const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit);
205
+
206
+ if (index !== -1){
207
+ stored_bookmark[index] = editTag;
208
+ await Storage.store("bookmark", stored_bookmark)
209
+
210
+ const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit)
211
+ for (const item of stored_comics){
212
+ await ComicStorage.replaceTag(item.source, item.id, editTag)
213
+ }
214
+
215
+
216
+
217
+
218
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit);
219
+ if (index_2 !== -1){
220
+ BOOKMARK_DATA[index_2].label = editTag
221
+ BOOKMARK_DATA[index_2].value = editTag
222
+ }
223
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
224
+ setManageBookmark({...manageBookmark,edit:""})
225
+ setEditTag("")
226
+
227
+ onRefresh();
228
+ }
229
+ setShowMenuOption({...showMenuOption,state:false,id:""})
230
+ }}
231
+ >
232
+
233
+ <Icon source={"check"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"green"}/>
234
+ </TouchableRipple>
235
+ </View>
236
+
237
+
238
+
239
+ </View>)
240
+
241
+ }</>
242
+
243
+
244
+
245
+
246
+ </View>
247
+
248
+ )
249
+ }
250
+ </>)
251
+ }
252
+ ,[Theme,themeTypeContext,manageBookmark,searchTag,removeTag,createTag,showMenuOption,MIGRATE_BOOKMARK_DATA,BOOKMARK_DATA])
253
+
254
+
255
+ const load_bookmark = async ()=>{
256
+ const stored_bookmark_data = await Storage.get("bookmark") || []
257
+ if (stored_bookmark_data.length) {
258
+ const bookmark_data:Array<Object> = []
259
+ for (const item of stored_bookmark_data) {
260
+ bookmark_data.push({
261
+ label:item,
262
+ value:item,
263
+ })
264
+ }
265
+
266
+ SET_BOOKMARK_DATA(bookmark_data.sort())
267
+ }else SET_BOOKMARK_DATA([])
268
+ }
269
+
270
+ useEffect(()=>{
271
+ SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA])
272
+ },[BOOKMARK_DATA])
273
+
274
+ useEffect(()=>{
275
+ load_bookmark()
276
+ return () => controller.abort();
277
+ },[])
278
+
279
+ return (<>{BOOKMARK_DATA !== null && <>
280
+
281
+ <View key={"BookmarkWidget"}
282
+ style={{
283
+ zIndex:10,
284
+ backgroundColor:Theme[themeTypeContext].background_color,
285
+ maxWidth:500,
286
+ width:"100%",
287
+
288
+ borderColor:Theme[themeTypeContext].border_color,
289
+ borderWidth:2,
290
+ borderRadius:8,
291
+ padding:12,
292
+ display:"flex",
293
+ justifyContent:"center",
294
+
295
+ flexDirection:"column",
296
+ gap:12,
297
+ }}
298
+ from={{
299
+ opacity: 0,
300
+ scale: 0.9,
301
+ }}
302
+ animate={{
303
+ opacity: 1,
304
+ scale: 1,
305
+ }}
306
+ exit={{
307
+ opacity: 0,
308
+ scale: 0.5,
309
+ }}
310
+ transition={{
311
+ type: 'timing',
312
+ duration: 500,
313
+ }}
314
+ exitTransition={{
315
+ type: 'timing',
316
+ duration: 250,
317
+ }}
318
+ >
319
+
320
+ <>{!createTag.state && !removeTag.state && <>
321
+ <View
322
+
323
+ style={{
324
+ display:"flex",
325
+ flexDirection:"column",
326
+ gap:18,
327
+ }}
328
+ >
329
+ <>{BOOKMARK_DATA.length
330
+ ? <>
331
+ <View
332
+ style={{flex:1}}
333
+ >
334
+ <TextInput mode="outlined" label="Search" textColor={Theme[themeTypeContext].text_color}
335
+
336
+ style={{
337
+
338
+ backgroundColor:Theme[themeTypeContext].background_color,
339
+ borderColor:Theme[themeTypeContext].border_color,
340
+
341
+ }}
342
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
343
+ value={searchTag}
344
+ onChange={(event)=>{
345
+ setSearchTag(event.nativeEvent.text)
346
+ }}
347
+ />
348
+ </View>
349
+ <View
350
+ style={{
351
+ maxHeight:Dimensions.height*0.7
352
+ }}
353
+ >
354
+ <ScrollView
355
+ contentContainerStyle={{
356
+ display:"flex",
357
+ flexDirection:"column",
358
+ justifyContent:"space-around",
359
+ gap:8,
360
+
361
+ height:"auto",
362
+ paddingVertical:12,
363
+ paddingHorizontal:8,
364
+ }}
365
+ style={{
366
+
367
+ }}
368
+ >
369
+ <>{BOOKMARK_DATA.map((item:any) =>
370
+ (
371
+ <View key={item.value}>
372
+ <RenderTag item={item}/>
373
+ <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].border_color}}/>
374
+ </View>
375
+ )
376
+ )}</>
377
+ </ScrollView>
378
+ </View>
379
+ </>
380
+ : <>
381
+ <Text style={{
382
+ width:"100%",
383
+ textAlign:"center",
384
+ color:Theme[themeTypeContext].text_color,
385
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.045,
386
+ fontFamily:"roboto-bold",
387
+ }}>No tag found</Text>
388
+ </>
389
+
390
+ }</>
391
+
392
+ <View
393
+ style={{
394
+ display:"flex",
395
+ flexDirection:"row",
396
+ width:"100%",
397
+ justifyContent:"space-around",
398
+ alignItems:"center",
399
+ }}
400
+ >
401
+ <Button mode='contained'
402
+ labelStyle={{
403
+ color:Theme[themeTypeContext].text_color,
404
+ fontFamily:"roboto-medium",
405
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
406
+ }}
407
+ style={{backgroundColor:"green",borderRadius:5}}
408
+ onPress={(()=>{
409
+ setCreateTag({state:true,title:""})
410
+ setShowMenuOption({...showMenuOption,state:false,id:""})
411
+ })}
412
+ >+ Create</Button>
413
+ <Button mode='outlined'
414
+ labelStyle={{
415
+ color:Theme[themeTypeContext].text_color,
416
+ fontFamily:"roboto-medium",
417
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
418
+
419
+
420
+ }}
421
+ style={{
422
+
423
+ borderRadius:5,
424
+ borderWidth:2,
425
+ borderColor:Theme[themeTypeContext].border_color
426
+ }}
427
+ onPress={(async ()=>{
428
+ setWidgetContext({state:false,component:<></>})
429
+ })}
430
+ >Done</Button>
431
+ </View>
432
+ </View>
433
+ </>}</>
434
+
435
+ <>{createTag.state &&
436
+ <>
437
+ <View
438
+ style={{
439
+ height:"auto",
440
+ display:"flex",
441
+ flexDirection:"column",
442
+ gap:12,
443
+ }}
444
+ >
445
+ <TextInput mode="outlined" label="Create Tag" textColor={Theme[themeTypeContext].text_color} maxLength={72}
446
+ placeholder="Bookmark Tag"
447
+
448
+ right={<TextInput.Affix text={`| Max: 72`} />}
449
+ style={{
450
+
451
+ backgroundColor:Theme[themeTypeContext].background_color,
452
+ borderColor:Theme[themeTypeContext].border_color,
453
+
454
+ }}
455
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
456
+ value={createTag.title}
457
+ onChange={(event)=>{
458
+ setCreateTag({...createTag,title:event.nativeEvent.text})
459
+ }}
460
+ />
461
+ </View>
462
+ <View
463
+ style={{
464
+ display:"flex",
465
+ flexDirection:"row",
466
+ width:"100%",
467
+ justifyContent:"space-around",
468
+ alignItems:"center",
469
+ }}
470
+ >
471
+ <Button mode='outlined'
472
+ labelStyle={{
473
+ color:Theme[themeTypeContext].text_color,
474
+ fontFamily:"roboto-medium",
475
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
476
+
477
+
478
+ }}
479
+ style={{
480
+
481
+ borderRadius:5,
482
+ borderWidth:2,
483
+ borderColor:Theme[themeTypeContext].border_color
484
+ }}
485
+ onPress={(()=>{
486
+
487
+ setCreateTag({...createTag,state:false})
488
+
489
+ })}
490
+ >Cancel</Button>
491
+ <Button mode='contained'
492
+ labelStyle={{
493
+ color:Theme[themeTypeContext].text_color,
494
+ fontFamily:"roboto-medium",
495
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
496
+ }}
497
+ style={{backgroundColor:"green",borderRadius:5}}
498
+ onPress={(async()=>{
499
+
500
+ const title = createTag.title
501
+ if (!title) return
502
+
503
+ const stored_bookmark_data = await Storage.get("bookmark") || []
504
+ if (stored_bookmark_data.includes(title)){
505
+ Toast.show({
506
+ type: 'error',
507
+ text1: '🔖 Duplicate Bookmark',
508
+ text2: `Tag "${title}" already existed in your bookmark.`,
509
+
510
+ position: "bottom",
511
+ visibilityTime: 5000,
512
+ text1Style:{
513
+ fontFamily:"roboto-bold",
514
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
515
+ },
516
+ text2Style:{
517
+ fontFamily:"roboto-medium",
518
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
519
+
520
+ },
521
+ });
522
+ }else{
523
+ await Storage.store("bookmark", [...stored_bookmark_data,title].sort())
524
+ SET_BOOKMARK_DATA([...BOOKMARK_DATA,
525
+ {label:title,value:title}
526
+ ].sort())
527
+ setCreateTag({state:false,title:""})
528
+ Toast.show({
529
+ type: 'info',
530
+ text1: '🔖 Create Bookmark',
531
+ text2: `Tag "${title}" added to your bookmark.`,
532
+
533
+ position: "bottom",
534
+ visibilityTime: 3000,
535
+ text1Style:{
536
+ fontFamily:"roboto-bold",
537
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
538
+ },
539
+ text2Style:{
540
+ fontFamily:"roboto-medium",
541
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
542
+
543
+ },
544
+ });
545
+ onRefresh()
546
+ }
547
+ })}
548
+ >Add</Button>
549
+ </View>
550
+ </>
551
+ }</>
552
+
553
+ </View>
554
+ <>{showMenuOption.state &&
555
+ <View
556
+ style={{
557
+ display:"flex",
558
+ position:"absolute",
559
+ zIndex:11,
560
+ justifyContent:"space-around",
561
+ flexDirection:"column",
562
+
563
+ backgroundColor:Theme[themeTypeContext].border_color,
564
+ top:showMenuOption.positions[0],
565
+ bottom:showMenuOption.positions[1],
566
+ left:showMenuOption.positions[2],
567
+ right:showMenuOption.positions[3],
568
+
569
+ width:(Dimensions.width+Dimensions.height)/2*0.2,
570
+ height:(Dimensions.width+Dimensions.height)/2*0.1,
571
+
572
+ borderRadius:5,
573
+ borderWidth:2,
574
+ borderColor:Theme[themeTypeContext].background_color
575
+ }}
576
+ >
577
+ <TouchableRipple
578
+
579
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
580
+ style={{
581
+
582
+ borderWidth:0,
583
+ backgroundColor: "transparent",
584
+ padding:5,
585
+ width:"100%",
586
+
587
+ }}
588
+
589
+ onPress={(event)=>{
590
+ setManageBookmark({...manageBookmark,edit:showMenuOption.id})
591
+ setShowMenuOption({...showMenuOption,state:false})
592
+ }}
593
+ >
594
+ <View
595
+ style={{
596
+ display:"flex",
597
+ flexDirection:"row",
598
+ justifyContent:"center",
599
+ alignItems:"center",
600
+ paddingHorizontal:18,
601
+
602
+ }}
603
+ >
604
+ <Icon source={"pencil"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"cyan"}/>
605
+ <View>
606
+ <Text selectable={false}
607
+ style={{
608
+ textAlign:"center",
609
+ color:"cyan",
610
+ fontFamily:"roboto-medium",
611
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
612
+ }}
613
+ >Edit</Text>
614
+ </View>
615
+ </View>
616
+ </TouchableRipple>
617
+ <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].background_color}}/>
618
+ <TouchableRipple
619
+
620
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
621
+ style={{
622
+
623
+ borderWidth:0,
624
+ backgroundColor: "transparent",
625
+ padding:5,
626
+ width:"100%",
627
+ }}
628
+
629
+ onPress={(event)=>{
630
+ setManageBookmark({...manageBookmark,edit:"",delete:showMenuOption.id})
631
+ setShowMenuOption({...showMenuOption,state:false})
632
+ }}
633
+ >
634
+ <View
635
+ style={{
636
+ display:"flex",
637
+ flexDirection:"row",
638
+ justifyContent:"center",
639
+ alignItems:"center",
640
+ paddingHorizontal:18,
641
+
642
+ }}
643
+ >
644
+ <Icon source={"trash-can"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"red"}/>
645
+ <View>
646
+ <Text selectable={false}
647
+ style={{
648
+ textAlign:"center",
649
+ color:"red",
650
+ fontFamily:"roboto-medium",
651
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
652
+ }}
653
+ >Delete</Text>
654
+ </View>
655
+ </View>
656
+ </TouchableRipple>
657
+
658
+ </View>
659
+
660
+ }</>
661
+ <>{manageBookmark.delete && (
662
+
663
+
664
+ <View
665
+ style={{
666
+ top:0,
667
+ left:0,
668
+ position:"absolute",
669
+ width:Dimensions.width,
670
+ height:Dimensions.height,
671
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
672
+ zIndex:11,
673
+ display:"flex",
674
+ justifyContent:"center",
675
+ alignItems:"center",
676
+ padding:15,
677
+ }}
678
+ >
679
+ <View
680
+ style={{
681
+ backgroundColor:Theme[themeTypeContext].background_color,
682
+ maxWidth:500,
683
+ width:"100%",
684
+ height:"auto",
685
+
686
+ borderColor:Theme[themeTypeContext].border_color,
687
+ borderWidth:2,
688
+ borderRadius:8,
689
+ padding:12,
690
+ display:"flex",
691
+ justifyContent:"center",
692
+
693
+ flexDirection:"column",
694
+ gap:18,
695
+ }}
696
+ >
697
+ <View
698
+ style={{
699
+ borderBottomWidth:2,
700
+ borderColor:Theme[themeTypeContext].border_color,
701
+ padding:8,
702
+ width:"100%",
703
+ }}
704
+ >
705
+ <Text
706
+ numberOfLines={1}
707
+ style={{
708
+ color:"red",
709
+ fontFamily:"roboto-bold",
710
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03,
711
+ textAlign:"center",
712
+ }}
713
+ >Delete Tag: "{manageBookmark.delete}"</Text>
714
+ </View>
715
+ <View
716
+ style={{
717
+ width:"100%",
718
+ display:"flex",
719
+ flexDirection:"column",
720
+ gap:12,
721
+ }}
722
+ >
723
+ <View style={{flex:1}}>
724
+ <Dropdown
725
+ theme_type={themeTypeContext}
726
+ Dimensions={Dimensions}
727
+
728
+ label='Migrate comics to tag'
729
+ data={MIGRATE_BOOKMARK_DATA.filter((item:any) => item.value !== manageBookmark.delete)}
730
+ value={migrateTag}
731
+ onChange={(async (item:any) => {
732
+ setMigrateTag(item.value)
733
+ })}
734
+ />
735
+ </View>
736
+ <>{!migrateTag && (
737
+ <Text
738
+ style={{
739
+ color:Theme[themeTypeContext].text_color,
740
+ fontFamily:"roboto-bold",
741
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
742
+ textAlign:"center",
743
+ }}
744
+ >Setting migration to None will remove all comics and chapters for this bookmark tag.</Text>
745
+ )}</>
746
+ <View
747
+ style={{
748
+ display:"flex",
749
+ flexDirection:"row",
750
+ width:"100%",
751
+ justifyContent:"space-around",
752
+ alignItems:"center",
753
+ }}
754
+ >
755
+ <>{migrateTag
756
+
757
+ ? <TouchableRipple
758
+
759
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
760
+ style={{
761
+
762
+ borderWidth:0,
763
+ backgroundColor: "blue",
764
+ padding:5,
765
+ borderRadius:8,
766
+ paddingHorizontal:12,
767
+ paddingVertical:8,
768
+
769
+
770
+ }}
771
+
772
+ onPress={async (event)=>{
773
+ const stored_bookmark = await Storage.get("bookmark")
774
+ const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete)
775
+ if (index === -1) return
776
+
777
+ const stored_comics = await ComicStorage.getByTag(manageBookmark.delete)
778
+ for (const comic of stored_comics) {
779
+ const source = comic.source;
780
+ const comic_id = comic.id
781
+ await ComicStorage.replaceTag(source,comic_id,migrateTag)
782
+
783
+ }
784
+
785
+ stored_bookmark.splice(index, 1);
786
+ await Storage.store("bookmark",stored_bookmark);
787
+
788
+
789
+
790
+
791
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete);
792
+ if (index_2 !== -1){
793
+ BOOKMARK_DATA.splice(index_2, 1);
794
+ }
795
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
796
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
797
+ setShowMenuOption({...showMenuOption,state:false,id:""})
798
+ setMigrateTag("")
799
+
800
+ onRefresh();
801
+ }}
802
+ >
803
+
804
+ <Text selectable={false}
805
+ style={{
806
+ textAlign:"center",
807
+ color:Theme[themeTypeContext].text_color,
808
+ fontFamily:"roboto-medium",
809
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
810
+ }}
811
+ >Migrate</Text>
812
+
813
+ </TouchableRipple>
814
+ : <TouchableRipple
815
+
816
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
817
+ style={{
818
+
819
+ borderWidth:0,
820
+ backgroundColor: "red",
821
+ padding:5,
822
+ borderRadius:8,
823
+ paddingHorizontal:12,
824
+ paddingVertical:8,
825
+
826
+
827
+ }}
828
+
829
+ onPress={async (event)=>{
830
+ const stored_bookmark = await Storage.get("bookmark");
831
+ const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete)
832
+ if (index === -1) return
833
+ await ComicStorage.removeByTag(manageBookmark.delete);
834
+ stored_bookmark.splice(index, 1);
835
+ await Storage.store("bookmark",stored_bookmark);
836
+
837
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete);
838
+ if (index_2 !== -1){
839
+ BOOKMARK_DATA.splice(index_2, 1);
840
+ }
841
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
842
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
843
+ setShowMenuOption({...showMenuOption,state:false,id:""})
844
+ setMigrateTag("")
845
+ onRefresh()
846
+
847
+ }}
848
+ >
849
+
850
+ <Text selectable={false}
851
+ style={{
852
+ textAlign:"center",
853
+ color:Theme[themeTypeContext].text_color,
854
+ fontFamily:"roboto-medium",
855
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
856
+ }}
857
+ >Delete</Text>
858
+
859
+ </TouchableRipple>
860
+ }</>
861
+ <TouchableRipple
862
+
863
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
864
+ style={{
865
+
866
+ borderWidth:2,
867
+ borderColor:Theme[themeTypeContext].border_color,
868
+ backgroundColor: "transparent",
869
+ padding:5,
870
+ borderRadius:8,
871
+ paddingHorizontal:12,
872
+ paddingVertical:8,
873
+
874
+
875
+ }}
876
+
877
+ onPress={(event)=>{
878
+ setShowMenuOption({...showMenuOption,state:false,id:""})
879
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
880
+ }}
881
+ >
882
+
883
+ <Text selectable={false}
884
+ style={{
885
+ textAlign:"center",
886
+ color:Theme[themeTypeContext].text_color,
887
+ fontFamily:"roboto-medium",
888
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
889
+ }}
890
+ >Cancel</Text>
891
+
892
+ </TouchableRipple>
893
+ </View>
894
+
895
+ </View>
896
+ </View>
897
+
898
+ </View>
899
+ )}</>
900
+
901
+ </>}</>)
902
+ }
903
+
904
+ export default BookmarkWidget;
frontend/app/bookmark/index.tsx ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react';
2
+ import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router';
3
+ import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper';
6
+ import CircularProgress from 'react-native-circular-progress-indicator';
7
+ import { ActivityIndicator } from 'react-native-paper';
8
+ import { FlashList } from "@shopify/flash-list";
9
+
10
+
11
+ import uuid from 'react-native-uuid';
12
+ import Toast from 'react-native-toast-message';
13
+ import { View, AnimatePresence } from 'moti';
14
+ import * as Clipboard from 'expo-clipboard';
15
+ import * as FileSystem from 'expo-file-system';
16
+ import NetInfo from "@react-native-community/netinfo";
17
+ import { Marquee } from '@animatereactnative/marquee';
18
+ import { Slider } from '@rneui/themed-edge';
19
+
20
+ import BookmarkComponent from './components/bookmark_component';
21
+ import ComicComponent from './components/comic_component';
22
+ import BookmarkWidget from './components/widgets/bookmark';
23
+
24
+ import { __styles } from './stylesheet/styles';
25
+ import Storage from '@/constants/module/storages/storage';
26
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
27
+ import Image from '@/components/Image';
28
+ import {CONTEXT} from '@/constants/module/context';
29
+ import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager";
30
+ import Theme from '@/constants/theme';
31
+ import ComicStorage from '@/constants/module/storages/comic_storage';
32
+
33
+ const Index = ({}:any) => {
34
+ const Dimensions = useWindowDimensions();
35
+ const controller = new AbortController();
36
+ const signal = controller.signal;
37
+
38
+ const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
39
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
40
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
41
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
42
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
43
+
44
+ const [styles, setStyles]:any = useState("")
45
+ const [isLoading, setIsLoading] = useState<boolean>(true)
46
+ const [onRefresh, setOnRefresh] = useState(false)
47
+ const [search, setSearch] = useState<any>({state:false,text:""})
48
+
49
+ const [BOOKMARK_DATA, SET_BOOKMARK_DATA]:any = useState([])
50
+ const [SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK] = useState<string>("")
51
+
52
+ const [COMIC_DATA, SET_COMIC_DATA] = useState<any>([])
53
+
54
+ useEffect(() => {
55
+ (async ()=>{
56
+ if (!SELECTED_BOOKMARK) return
57
+ const stored_comic = await ComicStorage.getByTag(SELECTED_BOOKMARK)
58
+ console.log(stored_comic)
59
+ SET_COMIC_DATA(stored_comic)
60
+ })()
61
+ },[SELECTED_BOOKMARK,onRefresh])
62
+
63
+ useFocusEffect(useCallback(() => {
64
+ (async ()=>{
65
+ setIsLoading(true)
66
+ const stored_bookmark = await Storage.get("bookmark") || []
67
+ console.log(stored_bookmark)
68
+
69
+ SET_BOOKMARK_DATA(stored_bookmark)
70
+ console.log("AA",stored_bookmark.length )
71
+ if (stored_bookmark.length) {
72
+ SET_SELECTED_BOOKMARK(stored_bookmark[0])
73
+ }
74
+ setIsLoading(false)
75
+ })()
76
+ },[onRefresh]))
77
+
78
+ const renderBookmarkComponent = useCallback(({item,index}:any) => {
79
+ return <BookmarkComponent key={index.toString()} item={item}
80
+ SELECTED_BOOKMARK={SELECTED_BOOKMARK} SET_SELECTED_BOOKMARK={SET_SELECTED_BOOKMARK}
81
+ />
82
+ },[SELECTED_BOOKMARK])
83
+
84
+ const renderComicComponent = useCallback(({item,index}:any) => {
85
+ return <ComicComponent key={index.toString()} item={item}
86
+
87
+ />
88
+ },[SELECTED_BOOKMARK])
89
+
90
+ useFocusEffect(useCallback(() => {
91
+ setIsLoading(true)
92
+ setShowMenuContext(true)
93
+ setStyles(__styles(themeTypeContext,Dimensions))
94
+
95
+ return () => {
96
+ controller.abort();
97
+ };
98
+ },[]))
99
+
100
+ return (<>{styles && ! isLoading
101
+ ? <>
102
+ <View style={styles.screen_container}
103
+
104
+ >
105
+ <View style={styles.header_container}>
106
+ <Text style={styles.header_text}>Bookmark</Text>
107
+
108
+ <View style={styles.header_button_box}>
109
+
110
+ <TouchableRipple
111
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
112
+ style={{
113
+ ...styles.header_search_button,
114
+ backgroundColor: search.state ? Theme[themeTypeContext].button_selected_color : "transparent",
115
+ }}
116
+
117
+ onPress={() => {
118
+ setSearch({...search,state:!search.state})
119
+ }}
120
+ >
121
+ <Icon source={"magnify"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={Theme[themeTypeContext].icon_color}/>
122
+ </TouchableRipple>
123
+ </View>
124
+
125
+
126
+ </View>
127
+ <>{search.state && (
128
+ <View
129
+ style={{
130
+ width:"100%",
131
+ height:"auto",
132
+ paddingHorizontal:12,
133
+ paddingVertical:18,
134
+ borderBottomWidth:2,
135
+ borderColor:Theme[themeTypeContext].border_color,
136
+ }}
137
+ >
138
+ <TextInput mode="outlined" label="Search" textColor={Theme[themeTypeContext].text_color}
139
+
140
+ style={{
141
+
142
+ backgroundColor:Theme[themeTypeContext].background_color,
143
+ borderColor:Theme[themeTypeContext].border_color,
144
+
145
+ }}
146
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
147
+ value={search.text}
148
+ onChange={(event)=>{
149
+ setSearch({...search,text:event.nativeEvent.text})
150
+ }}
151
+ />
152
+
153
+ </View>
154
+ )}</>
155
+ <View
156
+ style={{
157
+ display:"flex",
158
+ flexDirection:"row",
159
+ width:"100%",
160
+ height:"auto",
161
+ backgroundColor:"transparent",
162
+ borderBottomWidth:2,
163
+ borderColor:Theme[themeTypeContext].border_color,
164
+ }}
165
+ >
166
+ <FlatList
167
+ contentContainerStyle={{
168
+ flex:1,
169
+ flexGrow: 1,
170
+ justifyContent: 'center',
171
+ }}
172
+ horizontal={true}
173
+ data={BOOKMARK_DATA}
174
+ renderItem={renderBookmarkComponent}
175
+ ItemSeparatorComponent={undefined}
176
+ />
177
+ <TouchableRipple
178
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
179
+ style={{
180
+ borderRadius:5,
181
+ borderWidth:0,
182
+ backgroundColor: "transparent",
183
+ padding:5,
184
+ justifyContent:"center",
185
+ alignItems:"center",
186
+ }}
187
+ onPress={() => {
188
+ setWidgetContext({state:true,component:
189
+ <BookmarkWidget
190
+ setIsLoading={setIsLoading}
191
+ onRefresh={()=>{setOnRefresh(!onRefresh)}}
192
+
193
+ />
194
+ })
195
+ }}
196
+ >
197
+ <Icon source={require("@/assets/icons/tag-edit-outline.png")} size={((Dimensions.width+Dimensions.height)/2)*0.0325} color={Theme[themeTypeContext].icon_color}/>
198
+ </TouchableRipple>
199
+ </View>
200
+ <FlatList
201
+ contentContainerStyle={{
202
+ flexGrow: 1,
203
+ padding:12,
204
+ flexDirection:"row",
205
+ gap:Math.max((Dimensions.width+Dimensions.height)/2*0.015,8),
206
+ flexWrap:"wrap",
207
+ }}
208
+ renderItem={renderComicComponent}
209
+ ItemSeparatorComponent={undefined}
210
+ data={COMIC_DATA.filter((item:any) => item.info.title.toLowerCase().includes(search.text.toLowerCase()))}
211
+ ListEmptyComponent={
212
+ <View
213
+ style={{
214
+ width:"100%",
215
+ height:"100%",
216
+ backgroundColor:"transparent",
217
+ display:"flex",
218
+ justifyContent:"center",
219
+ alignItems:"center",
220
+ flexDirection:"row",
221
+ gap:12,
222
+ }}
223
+ >
224
+ <>{BOOKMARK_DATA.length
225
+ ? <>
226
+ {search.text && COMIC_DATA.length
227
+ ? <>
228
+ <Icon source={"magnify-scan"} color={Theme[themeTypeContext].icon_color} size={((Dimensions.width+Dimensions.height)/2)*0.03}/>
229
+ <Text selectable={false}
230
+ style={{
231
+ fontFamily:"roboto-bold",
232
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
233
+ color:Theme[themeTypeContext].text_color,
234
+ }}
235
+ >Search no result</Text>
236
+ </>
237
+ : <>
238
+ <Icon source={require("@/assets/icons/tag-hidden.png")} color={Theme[themeTypeContext].icon_color} size={((Dimensions.width+Dimensions.height)/2)*0.03}/>
239
+ <Text selectable={false}
240
+ style={{
241
+ fontFamily:"roboto-bold",
242
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
243
+ color:Theme[themeTypeContext].text_color,
244
+ }}
245
+ >This tag is empty.</Text>
246
+ </>
247
+ }
248
+ </>
249
+ : <View style={{
250
+ display:"flex",
251
+ flexDirection:"column",
252
+ justifyContent:"center",
253
+ alignItems:"center",
254
+ width:"100%",
255
+ height:"auto",
256
+ }}>
257
+
258
+ <Text selectable={false}
259
+ style={{
260
+ fontFamily:"roboto-bold",
261
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
262
+ color:Theme[themeTypeContext].text_color,
263
+ textAlign:"center",
264
+ }}
265
+ >
266
+ No tag found. {"\n"}Press{" "}
267
+ <Icon source={require("@/assets/icons/tag-edit-outline.png")} color={Theme[themeTypeContext].icon_color} size={((Dimensions.width+Dimensions.height)/2)*0.03}/>
268
+ {" "}to create bookmark tag.
269
+ </Text>
270
+ </View>
271
+
272
+ }
273
+ </>
274
+
275
+ </View>
276
+
277
+ }
278
+ />
279
+ </View>
280
+
281
+ </>
282
+ : <View style={{zIndex:5,width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",backgroundColor:Theme[themeTypeContext].background_color}}>
283
+ <Image setShowCloudflareTurnstile={setShowCloudflareTurnstileContext} source={require("@/assets/gif/cat-loading.gif")} style={{width:((Dimensions.width+Dimensions.height)/2)*0.15,height:((Dimensions.width+Dimensions.height)/2)*0.15}}/>
284
+ </View>
285
+ }</>)
286
+
287
+
288
+ }
289
+
290
+ export default Index;
291
+
frontend/app/bookmark/stylesheet/styles.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StyleSheet } from "react-native";
2
+ import Theme from "@/constants/theme";
3
+
4
+ export const __styles:any = (theme_type:string,Dimensions:any) => {
5
+ return StyleSheet.create({
6
+ screen_container: {
7
+ display: "flex",
8
+ width: "100%",
9
+ height: "100%",
10
+ backgroundColor: Theme[theme_type].background_color,
11
+ },
12
+ header_container: {
13
+ display: "flex",
14
+ flexDirection: "row",
15
+ justifyContent: "space-between",
16
+ alignItems: "center",
17
+ paddingHorizontal: 15,
18
+ paddingVertical:10,
19
+ backgroundColor: Theme[theme_type].background_color,
20
+ borderBottomWidth: 0.5,
21
+ borderColor: Theme[theme_type].border_color,
22
+ },
23
+ header_text:{
24
+ fontFamily: "roboto-medium",
25
+ fontSize: ((Dimensions.width+Dimensions.height)/2)*0.04,
26
+ color: Theme[theme_type].text_color,
27
+
28
+ },
29
+ header_search_button:{
30
+ borderRadius:5,
31
+ borderWidth:0,
32
+ padding:5,
33
+ },
34
+
35
+ item_box:{
36
+ display:"flex",
37
+ flexDirection:"column",
38
+ alignItems:"center",
39
+ gap:15,
40
+ height:"auto",
41
+ width:Math.max(((Dimensions.width+Dimensions.height)/2)*0.225,100),
42
+ borderRadius:8,
43
+
44
+ },
45
+ item_cover:{
46
+ width:"100%",
47
+ height:Math.max(((Dimensions.width+Dimensions.height)/2)*0.325,125),
48
+ borderRadius:8,
49
+ shadowColor: "#000",
50
+ shadowOffset: {
51
+ width: 0,
52
+ height: 1,
53
+ },
54
+ shadowOpacity: 0.22,
55
+ shadowRadius: 2.22,
56
+
57
+ elevation: 3,
58
+ },
59
+ item_title:{
60
+ color: Theme[theme_type].text_color,
61
+ fontFamily: "roboto-medium",
62
+ fontSize: ((Dimensions.width+Dimensions.height)/2)*0.025,
63
+ width:"100%",
64
+ height:"auto",
65
+ textAlign:"center",
66
+ flexShrink:1,
67
+ }
68
+ })}
frontend/app/explore/index.tsx CHANGED
@@ -9,7 +9,7 @@ import Dropdown from '@/components/dropdown';
9
 
10
 
11
  import Theme from '@/constants/theme';
12
- import { __styles } from './stylesheet/show_list_styles';
13
  import Storage from '@/constants/module/storages/storage';
14
  import ImageStorage from '@/constants/module/storages/image_cache_storage';
15
  import { CONTEXT } from '@/constants/module/context';
@@ -52,7 +52,7 @@ const Index = ({}:any) => {
52
  };
53
  }, []))
54
 
55
- useEffect(() => {
56
  (async ()=>{
57
  setStyles(__styles(themeTypeContext,Dimensions))
58
 
@@ -69,7 +69,7 @@ const Index = ({}:any) => {
69
  return () => {
70
  controller.abort();
71
  };
72
- },[])
73
 
74
 
75
 
@@ -397,7 +397,7 @@ const Index = ({}:any) => {
397
  contentFit="cover" transition={1000}
398
  />
399
 
400
- <Text style={styles.item_title}>{item.title}</Text>
401
  </>
402
  </TouchableRipple>
403
 
 
9
 
10
 
11
  import Theme from '@/constants/theme';
12
+ import { __styles } from './stylesheet/styles';
13
  import Storage from '@/constants/module/storages/storage';
14
  import ImageStorage from '@/constants/module/storages/image_cache_storage';
15
  import { CONTEXT } from '@/constants/module/context';
 
52
  };
53
  }, []))
54
 
55
+ useFocusEffect(useCallback(() => {
56
  (async ()=>{
57
  setStyles(__styles(themeTypeContext,Dimensions))
58
 
 
69
  return () => {
70
  controller.abort();
71
  };
72
+ },[]))
73
 
74
 
75
 
 
397
  contentFit="cover" transition={1000}
398
  />
399
 
400
+ <Text selectable={false} style={styles.item_title}>{item.title}</Text>
401
  </>
402
  </TouchableRipple>
403
 
frontend/app/explore/stylesheet/{show_list_styles.tsx → styles.tsx} RENAMED
File without changes
frontend/app/index.tsx CHANGED
@@ -9,7 +9,7 @@ const Index = () => {
9
  const pathname = usePathname()
10
 
11
  if (pathname === "/" || pathname === "") return (
12
- <Redirect href="/view/colamanga/manga-wp55334" />
13
  )
14
 
15
  }
 
9
  const pathname = usePathname()
10
 
11
  if (pathname === "/" || pathname === "") return (
12
+ <Redirect href="/bookmark" />
13
  )
14
 
15
  }
frontend/app/read/[source]/[comic_id]/[chapter_idx].tsx CHANGED
@@ -16,6 +16,7 @@ import * as FileSystem from 'expo-file-system';
16
  import NetInfo from "@react-native-community/netinfo";
17
  import { Marquee } from '@animatereactnative/marquee';
18
  import { Slider } from '@rneui/themed-edge';
 
19
 
20
  import Storage from '@/constants/module/storages/storage';
21
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
@@ -28,12 +29,12 @@ import Menu from '../../components/menu/menu';
28
  import Disqus from '../../components/disqus';
29
  import { get_chapter } from '../../modules/get_chapter';
30
  import ComicStorage from '@/constants/module/storages/comic_storage';
 
31
 
32
  const Index = ({}:any) => {
33
  const SOURCE = useLocalSearchParams().source;
34
  const COMIC_ID = useLocalSearchParams().comic_id;
35
  const Dimensions = useWindowDimensions();
36
- const StaticDimensions = useMemo(() => Dimensions, [])
37
 
38
 
39
  const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
@@ -46,9 +47,11 @@ const Index = ({}:any) => {
46
  const [chapterInfo, setChapterInfo]:any = useState({})
47
  const [showOptions, setShowOptions]:any = useState({type:"general",state:false})
48
  const [DATA, SET_DATA]:any = useState([])
49
- const [isAdding, setIsAdding]:any = useState(false)
 
50
  const [zoom, setZoom]:any = useState(0)
51
 
 
52
  const CHAPTER_IDX = useRef(Number(useLocalSearchParams().chapter_idx as string));
53
 
54
 
@@ -60,6 +63,27 @@ const Index = ({}:any) => {
60
 
61
  // First Load
62
  useEffect(()=>{(async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  if (!SOURCE || !COMIC_ID || !CHAPTER_IDX.current){
64
  setIsError({state:true,text:"Invalid source, comic id or chapter idx!"})
65
  return
@@ -94,39 +118,8 @@ const Index = ({}:any) => {
94
 
95
  const renderItem = useCallback(({item,index}:any) => {
96
  return <ChapterImage key={index} item={item} zoom={zoom} showOptions={showOptions} setShowOptions={setShowOptions} setIsLoading={setIsLoading} SET_DATA={SET_DATA}/>
97
- },[zoom,showOptions,setShowOptions])
98
-
99
- const onViewableItemsChanged = useCallback(async ({viewableItems, changed}:any) => {
100
- // const expect_chapter_idx = [CHAPTER_IDX.current + 1, CHAPTER_IDX.current - 1]
101
- // const current_count = viewableItems.filter((data:any) => data.item.chapter_idx === CHAPTER_IDX.current).length
102
- // const existed_count = viewableItems.filter((data:any) => expect_chapter_idx.includes(data.item.chapter_idx)).length
103
-
104
- // if (current_count || existed_count){
105
- // const choose_idx = current_count > existed_count ? CHAPTER_IDX.current : viewableItems.find((data:any) => expect_chapter_idx.includes(data.item.chapter_idx))?.item.chapter_idx
106
- // if (choose_idx === CHAPTER_IDX.current) return
107
- // const stored_chapter = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,choose_idx, {exclude_fields:["data"]})
108
- // setChapterInfo({
109
- // chapter_id: stored_chapter?.id,
110
- // chapter_idx: stored_chapter?.id,
111
- // title: stored_chapter?.title,
112
- // })
113
- // const stored_comic = await ComicStorage.getByID(SOURCE, COMIC_ID)
114
- // if (stored_comic.history.idx && choose_idx > stored_comic.history.idx) {
115
- // await ComicStorage.updateHistory(SOURCE, COMIC_ID, {idx:stored_chapter?.idx,id:stored_chapter?.id,title:stored_chapter?.title})
116
- // }
117
- // router.setParams({idx:choose_idx})
118
- // CHAPTER_IDX.current = choose_idx
119
- // }
120
- },[])
121
-
122
- const onEndReached = useCallback(async () => {
123
- console.log(DATA)
124
- // const NEW_DATA = DATA.filter((data:any) => data.chapter_idx === CHAPTER_IDX.current-2)
125
-
126
- // const chapter_current_data = await get_chapter(SOURCE,COMIC_ID,CHAPTER_IDX.current+1)
127
-
128
- // SET_DATA([...NEW_DATA,...chapter_current_data])
129
- },[DATA])
130
 
131
  return (<>
132
  {isError.state
@@ -167,7 +160,7 @@ const Index = ({}:any) => {
167
  }}
168
 
169
  onPress={()=>{
170
- router.replace("/explore")
171
  }}
172
  >
173
  <Icon source={"arrow-left-thin"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
@@ -220,18 +213,15 @@ const Index = ({}:any) => {
220
  zIndex:0
221
  }}
222
  >
223
- <FlatList
224
  data={DATA}
225
  renderItem={renderItem}
226
- // onEndReachedThreshold={0.5}
227
  windowSize={21}
228
  ItemSeparatorComponent={undefined}
229
- onEndReached={onEndReached}
230
- onViewableItemsChanged={onViewableItemsChanged}
231
  />
232
  </View>
233
  <AnimatePresence exitBeforeEnter>
234
- {showOptions.state &&
235
  <View
236
  style={{
237
  position:"absolute",
@@ -304,7 +294,7 @@ const Index = ({}:any) => {
304
  }}
305
 
306
  onPress={()=>{
307
- router.replace(`/view/${SOURCE}/${COMIC_ID}/`)
308
  }}
309
  >
310
  <Icon source={"arrow-left-thin"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
@@ -325,21 +315,7 @@ const Index = ({}:any) => {
325
  {chapterInfo.title}
326
  </Text>
327
  </View>
328
- <TouchableRipple
329
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
330
- style={{
331
- borderRadius:5,
332
- borderWidth:0,
333
- backgroundColor: "transparent",
334
- padding:5,
335
- }}
336
-
337
- onPress={()=>{
338
- console.log("HO2h2")
339
- }}
340
- >
341
- <Icon source={"cloud-refresh"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
342
- </TouchableRipple>
343
  </View>
344
  <View
345
  style={{
 
16
  import NetInfo from "@react-native-community/netinfo";
17
  import { Marquee } from '@animatereactnative/marquee';
18
  import { Slider } from '@rneui/themed-edge';
19
+ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc);
20
 
21
  import Storage from '@/constants/module/storages/storage';
22
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
 
29
  import Disqus from '../../components/disqus';
30
  import { get_chapter } from '../../modules/get_chapter';
31
  import ComicStorage from '@/constants/module/storages/comic_storage';
32
+ import { set } from 'lodash';
33
 
34
  const Index = ({}:any) => {
35
  const SOURCE = useLocalSearchParams().source;
36
  const COMIC_ID = useLocalSearchParams().comic_id;
37
  const Dimensions = useWindowDimensions();
 
38
 
39
 
40
  const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
 
47
  const [chapterInfo, setChapterInfo]:any = useState({})
48
  const [showOptions, setShowOptions]:any = useState({type:"general",state:false})
49
  const [DATA, SET_DATA]:any = useState([])
50
+
51
+
52
  const [zoom, setZoom]:any = useState(0)
53
 
54
+
55
  const CHAPTER_IDX = useRef(Number(useLocalSearchParams().chapter_idx as string));
56
 
57
 
 
63
 
64
  // First Load
65
  useEffect(()=>{(async () => {
66
+ setIsLoading(true)
67
+
68
+ const stored_recent = await Storage.get("RECENT") || []
69
+ stored_recent.sort((a:any,b:any) => b.timestamp - a.timestamp)
70
+
71
+ let exist = false
72
+ for (const i of stored_recent) {
73
+ if (i.source === SOURCE && i.comic_id === COMIC_ID) {
74
+ i.timestamp = dayjs().utc().valueOf()
75
+ exist = true
76
+ break
77
+ }
78
+ }
79
+ if (!exist) {
80
+ if (stored_recent.length >= 25) stored_recent.pop()
81
+ stored_recent.push({source:SOURCE,comic_id:COMIC_ID,timestamp:dayjs().utc().valueOf()})
82
+ }
83
+
84
+ await Storage.store("RECENT",stored_recent)
85
+
86
+
87
  if (!SOURCE || !COMIC_ID || !CHAPTER_IDX.current){
88
  setIsError({state:true,text:"Invalid source, comic id or chapter idx!"})
89
  return
 
118
 
119
  const renderItem = useCallback(({item,index}:any) => {
120
  return <ChapterImage key={index} item={item} zoom={zoom} showOptions={showOptions} setShowOptions={setShowOptions} setIsLoading={setIsLoading} SET_DATA={SET_DATA}/>
121
+ },[zoom,showOptions])
122
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  return (<>
125
  {isError.state
 
160
  }}
161
 
162
  onPress={()=>{
163
+ router.replace(`/view/${SOURCE}/${COMIC_ID}/?mode=local`)
164
  }}
165
  >
166
  <Icon source={"arrow-left-thin"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
 
213
  zIndex:0
214
  }}
215
  >
216
+ <FlatList
217
  data={DATA}
218
  renderItem={renderItem}
 
219
  windowSize={21}
220
  ItemSeparatorComponent={undefined}
 
 
221
  />
222
  </View>
223
  <AnimatePresence exitBeforeEnter>
224
+ {showOptions.state &&
225
  <View
226
  style={{
227
  position:"absolute",
 
294
  }}
295
 
296
  onPress={()=>{
297
+ router.replace(`/view/${SOURCE}/${COMIC_ID}/?mode=local`)
298
  }}
299
  >
300
  <Icon source={"arrow-left-thin"} size={((Dimensions.width+Dimensions.height)/2)*0.05} color={Theme[themeTypeContext].icon_color}/>
 
315
  {chapterInfo.title}
316
  </Text>
317
  </View>
318
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  </View>
320
  <View
321
  style={{
frontend/app/read/_layout.tsx CHANGED
@@ -2,7 +2,6 @@ import { Tabs, Stack } from 'expo-router';
2
  import React from 'react';
3
  import {View, Text} from 'react-native';
4
 
5
- import { TabBarIcon } from '@/components/navigation/TabBarIcon';
6
  import { Colors } from '@/constants/Colors';
7
  import { useColorScheme } from '@/hooks/useColorScheme';
8
  import { SafeAreaView } from 'react-native-safe-area-context';
 
2
  import React from 'react';
3
  import {View, Text} from 'react-native';
4
 
 
5
  import { Colors } from '@/constants/Colors';
6
  import { useColorScheme } from '@/hooks/useColorScheme';
7
  import { SafeAreaView } from 'react-native-safe-area-context';
frontend/app/read/components/chapter_image.tsx CHANGED
@@ -14,6 +14,7 @@ import * as FileSystem from 'expo-file-system';
14
  import NetInfo from "@react-native-community/netinfo";
15
  import JSZip from 'jszip';
16
 
 
17
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
18
  import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage';
19
  import Image from '@/components/Image';
@@ -22,7 +23,7 @@ import {blobToBase64, base64ToBlob} from "@/constants/module/file_manager";
22
  import Theme from '@/constants/theme';
23
  import { get_chapter } from '../modules/get_chapter';
24
 
25
- const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET_DATA}:any)=>{
26
  const SOURCE = useLocalSearchParams().source;
27
  const COMIC_ID = useLocalSearchParams().comic_id;
28
  const CHAPTER_IDX = Number(useLocalSearchParams().chapter_idx as string);
@@ -34,6 +35,7 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
34
 
35
  const [isReady, setIsReady] = useState(false);
36
  const [isError, setIsError] = useState({state:false,text:""});
 
37
 
38
  const image = useRef<any>(null);
39
  const image_layout = useRef<any>(null);
@@ -53,9 +55,17 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
53
 
54
  })()},[])
55
 
 
 
 
 
 
 
56
 
57
  return ( <Pressable
58
- onPress={()=>{setShowOptions({type:"general",state:!showOptions.state})}}
 
 
59
  style={{
60
  display:"flex",
61
  width:"100%",
@@ -76,6 +86,7 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
76
  aspectRatio: image_layout.current.width / image_layout.current.height,
77
  }}
78
  onLoadEnd={()=>{
 
79
  }}
80
  />
81
  )}
@@ -171,7 +182,7 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
171
  paddingVertical: 18,
172
  }}
173
  >
174
- <TouchableRipple
175
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
176
  style={{
177
  width:"auto",
@@ -192,8 +203,10 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
192
 
193
  }}
194
  onPress={async ()=>{
 
195
  const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx-1)
196
  if (stored_chapter_info?.data_state === "completed"){
 
197
  router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`)
198
  }else{
199
  Toast.show({
@@ -214,18 +227,19 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
214
  },
215
  });
216
  }
 
217
  }}
218
  >
219
  <Text selectable={false}
220
  style={{
221
  color:Theme[themeTypeContext].text_color,
222
  fontFamily:"roboto-medium",
223
- fontSize:(Dimensions.width+Dimensions.height)/2*0.03
224
  }}
225
  >Previous</Text>
226
  </TouchableRipple>
227
 
228
- <TouchableRipple
229
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
230
  style={{
231
  width:"auto",
@@ -245,8 +259,16 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
245
  elevation: 5,
246
  }}
247
  onPress={async ()=>{
 
248
  const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx+1)
249
  if (stored_chapter_info?.data_state === "completed"){
 
 
 
 
 
 
 
250
  router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`)
251
  }else{
252
  Toast.show({
@@ -267,13 +289,14 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET
267
  },
268
  });
269
  }
 
270
  }}
271
  >
272
  <Text selectable={false}
273
  style={{
274
  color:Theme[themeTypeContext].text_color,
275
  fontFamily:"roboto-medium",
276
- fontSize:(Dimensions.width+Dimensions.height)/2*0.03
277
  }}
278
  >Next</Text>
279
  </TouchableRipple>
 
14
  import NetInfo from "@react-native-community/netinfo";
15
  import JSZip from 'jszip';
16
 
17
+ import ComicStorage from '@/constants/module/storages/comic_storage';
18
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
19
  import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage';
20
  import Image from '@/components/Image';
 
23
  import Theme from '@/constants/theme';
24
  import { get_chapter } from '../modules/get_chapter';
25
 
26
+ const ChapterImage = ({item, zoom, showOptions, setShowOptions, setIsLoading, SET_DATA}:any)=>{
27
  const SOURCE = useLocalSearchParams().source;
28
  const COMIC_ID = useLocalSearchParams().comic_id;
29
  const CHAPTER_IDX = Number(useLocalSearchParams().chapter_idx as string);
 
35
 
36
  const [isReady, setIsReady] = useState(false);
37
  const [isError, setIsError] = useState({state:false,text:""});
38
+ const [isNavigate, setIsNavigate] = useState(false);
39
 
40
  const image = useRef<any>(null);
41
  const image_layout = useRef<any>(null);
 
55
 
56
  })()},[])
57
 
58
+ useFocusEffect(useCallback(() => {
59
+ return () => {
60
+ image.current = null
61
+ };
62
+ },[]))
63
+
64
 
65
  return ( <Pressable
66
+ onPress={()=>{
67
+ setShowOptions({type:"general",state:!showOptions.state})
68
+ }}
69
  style={{
70
  display:"flex",
71
  width:"100%",
 
86
  aspectRatio: image_layout.current.width / image_layout.current.height,
87
  }}
88
  onLoadEnd={()=>{
89
+ image.current = ""
90
  }}
91
  />
92
  )}
 
182
  paddingVertical: 18,
183
  }}
184
  >
185
+ <TouchableRipple disabled={isNavigate}
186
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
187
  style={{
188
  width:"auto",
 
203
 
204
  }}
205
  onPress={async ()=>{
206
+ setIsNavigate(true);
207
  const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx-1)
208
  if (stored_chapter_info?.data_state === "completed"){
209
+ setIsLoading(true);
210
  router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`)
211
  }else{
212
  Toast.show({
 
227
  },
228
  });
229
  }
230
+ setIsNavigate(false);
231
  }}
232
  >
233
  <Text selectable={false}
234
  style={{
235
  color:Theme[themeTypeContext].text_color,
236
  fontFamily:"roboto-medium",
237
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03 * (1 - zoom / 100)
238
  }}
239
  >Previous</Text>
240
  </TouchableRipple>
241
 
242
+ <TouchableRipple disabled={isNavigate}
243
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
244
  style={{
245
  width:"auto",
 
259
  elevation: 5,
260
  }}
261
  onPress={async ()=>{
262
+ setIsNavigate(true);
263
  const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx+1)
264
  if (stored_chapter_info?.data_state === "completed"){
265
+ setIsLoading(true)
266
+
267
+ const stored_comic = await ComicStorage.getByID(SOURCE, COMIC_ID)
268
+ if (stored_comic.history.idx && item.chapter_idx+1 > stored_comic.history.idx) {
269
+ await ComicStorage.updateHistory(SOURCE, COMIC_ID, {idx:stored_chapter_info?.idx, id:stored_chapter_info?.id, title:stored_chapter_info?.title})
270
+ }
271
+
272
  router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`)
273
  }else{
274
  Toast.show({
 
289
  },
290
  });
291
  }
292
+ setIsNavigate(false);
293
  }}
294
  >
295
  <Text selectable={false}
296
  style={{
297
  color:Theme[themeTypeContext].text_color,
298
  fontFamily:"roboto-medium",
299
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03 * (1 - zoom / 100)
300
  }}
301
  >Next</Text>
302
  </TouchableRipple>
frontend/app/read/components/disqus.tsx CHANGED
@@ -17,15 +17,10 @@ const Disqus = ({url,identifier,title, paddingVertical=0, paddingHorizontal=0}:a
17
  const Dimensions = useWindowDimensions();
18
  const shortname = 'comicmtl';
19
 
20
-
21
- const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
22
  const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
23
- const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
24
- const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
25
 
26
  if (Platform.OS === "web") {
27
-
28
-
29
  return (
30
  <ScrollView
31
  style={{
 
17
  const Dimensions = useWindowDimensions();
18
  const shortname = 'comicmtl';
19
 
 
 
20
  const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
21
+
 
22
 
23
  if (Platform.OS === "web") {
 
 
24
  return (
25
  <ScrollView
26
  style={{
frontend/app/recent/_layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Tabs, Stack } from 'expo-router';
2
+ import React from 'react';
3
+ import {View, Text} from 'react-native';
4
+
5
+ import { Colors } from '@/constants/Colors';
6
+ import { useColorScheme } from '@/hooks/useColorScheme';
7
+ import { SafeAreaView } from 'react-native-safe-area-context';
8
+ export default function TabLayout() {
9
+ const colorScheme = useColorScheme();
10
+
11
+ return (
12
+ <Stack
13
+ screenOptions={{
14
+ headerShown: false,
15
+ }}>
16
+
17
+ </Stack>
18
+ );
19
+ }
frontend/app/recent/components/comic_component.tsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react';
2
+ import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router';
3
+ import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper';
6
+ import CircularProgress from 'react-native-circular-progress-indicator';
7
+ import { ActivityIndicator } from 'react-native-paper';
8
+ import { FlashList } from "@shopify/flash-list";
9
+
10
+
11
+ import uuid from 'react-native-uuid';
12
+ import Toast from 'react-native-toast-message';
13
+ import { View, AnimatePresence } from 'moti';
14
+ import * as Clipboard from 'expo-clipboard';
15
+ import * as FileSystem from 'expo-file-system';
16
+ import NetInfo from "@react-native-community/netinfo";
17
+ import { Marquee } from '@animatereactnative/marquee';
18
+ import { Slider } from '@rneui/themed-edge';
19
+
20
+ import { __styles } from '../stylesheet/styles';
21
+ import Storage from '@/constants/module/storages/storage';
22
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
23
+ import CoverStorage from '@/constants/module/storages/cover_storage';
24
+
25
+ import Image from '@/components/Image';
26
+ import {CONTEXT} from '@/constants/module/context';
27
+ import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager";
28
+ import Theme from '@/constants/theme';
29
+ import ComicStorage from '@/constants/module/storages/comic_storage';
30
+
31
+ const ComicComponent = ({index, item, SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK}:any) => {
32
+ const Dimensions = useWindowDimensions();
33
+ const controller = new AbortController();
34
+ const signal = controller.signal;
35
+
36
+ const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
37
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
38
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
39
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
40
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
41
+
42
+ const [styles, setStyles]:any = useState(null)
43
+ const [isLoading, setIsLoading] = useState<boolean>(true)
44
+
45
+ const cover:any = useRef("")
46
+
47
+ useFocusEffect(useCallback(() => {
48
+ (async ()=>{
49
+ setIsLoading(true)
50
+ setStyles(__styles(themeTypeContext,Dimensions))
51
+ const stored_bookmark = await Storage.get("bookmark") || []
52
+ console.log(stored_bookmark)
53
+ cover.current = await CoverStorage.get(`${item.source}-${item.id}`) || ""
54
+ setIsLoading(false)
55
+ })()
56
+
57
+ return () => {
58
+ cover.current = ""
59
+ controller.abort();
60
+ };
61
+ },[]))
62
+
63
+ return (<>{styles && !isLoading && <>
64
+ <>{index === 0
65
+ ? <View
66
+ style={{
67
+ width:"100%",
68
+ height:"auto",
69
+ paddingHorizontal:0,
70
+ paddingVertical:18,
71
+ borderBottomWidth:2,
72
+ borderColor:Theme[themeTypeContext].border_color,
73
+ display:"flex",
74
+ flexDirection:Dimensions.width >= 700 ? "row" : "column",
75
+ justifyContent:"space-around",
76
+ alignItems:"center",
77
+ gap:12,
78
+ }}
79
+ >
80
+
81
+ <TouchableRipple
82
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
83
+ onPress={()=>{router.navigate(`/view/${item.source}/${item.id}/?mode=local`)}}
84
+ style={{...styles.item_box, marginHorizontal:12,}}
85
+ >
86
+ <>
87
+ <Image onError={(error:any)=>{console.log("load image error",error)}} source={cover.current}
88
+ style={styles.item_cover}
89
+ contentFit="cover" transition={1000}
90
+ onLoadEnd={()=>{cover.current = ""}}
91
+ />
92
+ </>
93
+ </TouchableRipple>
94
+ <View
95
+ style={{
96
+ flex:1,
97
+ width:"auto",
98
+ height:Dimensions.width >= 700 ? "100%" : "auto",
99
+ paddingVertical:12,
100
+ alignItems:"center",
101
+ justifyContent:"space-around",
102
+ gap:12,
103
+ }}
104
+ >
105
+ <Text style={{
106
+ color:Theme[themeTypeContext].text_color,
107
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.035,
108
+ fontFamily:"roboto-bold",
109
+ textAlign:"center",
110
+ width:"100%",
111
+ }}>{item.info.title}</Text>
112
+
113
+ <TouchableRipple
114
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
115
+ style={{
116
+ width:Dimensions.width*0.40,
117
+ display:"flex",
118
+ flexDirection:"column",
119
+ justifyContent:"center",
120
+ padding:8,
121
+ borderRadius:Dimensions.width*0.60/2,
122
+ backgroundColor:Theme[themeTypeContext].border_color,
123
+
124
+ shadowColor: Theme[themeTypeContext].shadow_color,
125
+ shadowOffset: { width: 0, height: 2 },
126
+ shadowOpacity: 0.25,
127
+ shadowRadius: 3.84,
128
+ elevation: 5,
129
+
130
+ }}
131
+ onPress={()=>{
132
+ router.replace(`/read/${item.source}/${item.id}/${item.history.idx}/`)
133
+ }}
134
+ >
135
+ <View
136
+ style={{
137
+ display:"flex",
138
+ flexDirection:"column",
139
+ gap:12,
140
+ alignItems:"center",
141
+ }}
142
+ >
143
+ <Text selectable={false}
144
+ numberOfLines={1}
145
+ style={{
146
+ color:Theme[themeTypeContext].text_color,
147
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0225,
148
+ fontFamily:"roboto-bold",
149
+ }}
150
+ >
151
+ Continue
152
+ </Text>
153
+ <Text selectable={false}
154
+ numberOfLines={1}
155
+ style={{
156
+ color:Theme[themeTypeContext].text_color,
157
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.02,
158
+ fontFamily:"roboto-bold",
159
+ }}
160
+ >
161
+ {item.history.title}
162
+ </Text>
163
+ </View>
164
+ </TouchableRipple>
165
+
166
+
167
+
168
+ </View>
169
+
170
+ </View>
171
+
172
+ : <TouchableRipple
173
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
174
+ onPress={()=>{router.navigate(`/view/${item.source}/${item.id}/?mode=local`)}}
175
+ style={styles.item_box}
176
+ >
177
+ <>
178
+ <Image onError={(error:any)=>{console.log("load image error",error)}} source={cover.current}
179
+ style={styles.item_cover}
180
+ contentFit="cover" transition={1000}
181
+ onLoadEnd={()=>{cover.current = ""}}
182
+ />
183
+
184
+ <Text style={styles.item_title}>{item.info.title}</Text>
185
+ </>
186
+ </TouchableRipple>
187
+ }</>
188
+ </>}</>)
189
+
190
+
191
+ }
192
+
193
+ export default ComicComponent;
194
+
frontend/app/recent/components/widgets/bookmark.tsx ADDED
@@ -0,0 +1,904 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useEffect, useState, useCallback, useContext, useRef, Fragment, memo } from 'react';
3
+ import { Platform, useWindowDimensions, ScrollView } from 'react-native';
4
+
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper';
6
+ import { View, AnimatePresence } from 'moti';
7
+ import Toast from 'react-native-toast-message';
8
+ import * as FileSystem from 'expo-file-system';
9
+ import axios from 'axios';
10
+
11
+
12
+ import Theme from '@/constants/theme';
13
+ import Dropdown from '@/components/dropdown';
14
+ import { CONTEXT } from '@/constants/module/context';
15
+ import Storage from '@/constants/module/storages/storage';
16
+ import ComicStorage from '@/constants/module/storages/comic_storage';
17
+ import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
18
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
19
+ import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage';
20
+
21
+ interface BookmarkWidgetProps {
22
+ setIsLoading: any;
23
+ onRefresh: any;
24
+ }
25
+
26
+ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
27
+ setIsLoading,
28
+ onRefresh,
29
+ }) => {
30
+ const Dimensions = useWindowDimensions();
31
+
32
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
33
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
34
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
35
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
36
+
37
+ const [BOOKMARK_DATA, SET_BOOKMARK_DATA]: any = useState([])
38
+ const [MIGRATE_BOOKMARK_DATA, SET_MIGRATE_BOOKMARK_DATA]:any = useState([])
39
+
40
+
41
+ const [showMenuOption, setShowMenuOption]:any = useState({state:false,positions:[0,0,0,0],id:""})
42
+ const [searchTag, setSearchTag]:any = useState("")
43
+
44
+ const [migrateTag,setMigrateTag]:any = useState("")
45
+
46
+ const [manageBookmark, setManageBookmark]:any = useState({edit:"",delete:""})
47
+ const [createTag, setCreateTag]:any = useState({state:false,title:""})
48
+ const [removeTag, setRemoveTag]:any = useState({state:false, removing: false})
49
+
50
+
51
+ const controller = new AbortController();
52
+ const signal = controller.signal;
53
+
54
+ const RenderTag = useCallback(({item}:any) =>{
55
+ const [editTag, setEditTag]:any = useState(item.value)
56
+ useEffect(()=>{
57
+ },[manageBookmark])
58
+
59
+ return (<>
60
+ {item.value.includes(searchTag) &&
61
+ (
62
+ <View
63
+ style={{
64
+ display:"flex",
65
+ flexDirection:"row",
66
+ alignItems:"center",
67
+ justifyContent:"space-between",
68
+ gap:8,
69
+ zIndex:10,
70
+ }}
71
+ >
72
+ <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value &&
73
+ (<View
74
+ style={{
75
+ width:"100%",
76
+ display:"flex",
77
+ flexDirection:"row",
78
+ justifyContent:"space-between",
79
+ alignItems:"center",
80
+ height:"auto",
81
+ gap:18,
82
+ }}
83
+ >
84
+ <Text
85
+ style={{
86
+ color:"white",
87
+ fontFamily:"roboto-medium",
88
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.025
89
+ }}
90
+ >{item.label}</Text>
91
+ <View
92
+ style={{
93
+ width:"auto",
94
+ height:"auto",
95
+
96
+ }}
97
+ >
98
+
99
+ <TouchableRipple
100
+
101
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
102
+ style={{
103
+ borderRadius:5,
104
+ borderWidth:0,
105
+ backgroundColor: "transparent",
106
+ padding:5,
107
+
108
+ }}
109
+
110
+ onPress={(event)=>{
111
+ if (manageBookmark.edit){
112
+ setManageBookmark({...manageBookmark,edit:""})
113
+ setEditTag("")
114
+ }
115
+
116
+ const x = event.nativeEvent.pageX
117
+ const y = event.nativeEvent.pageY
118
+
119
+ setShowMenuOption({
120
+ ...showMenuOption,
121
+ state: showMenuOption.id === item.value ? false : true,
122
+ positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0],
123
+ id:showMenuOption.id === item.value ? "" : item.value,
124
+ })
125
+
126
+
127
+
128
+ }}
129
+ >
130
+
131
+ <Icon source={"dots-vertical"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={Theme[themeTypeContext].icon_color}/>
132
+ </TouchableRipple>
133
+ </View>
134
+ </View>)
135
+ }</>
136
+ <>{manageBookmark.edit === item.value &&
137
+ (<View
138
+ style={{
139
+ display:"flex",
140
+ flexDirection:"row",
141
+ justifyContent:"space-between",
142
+ alignItems:"center",
143
+ width:"100%",
144
+ height:"auto",
145
+ gap:12,
146
+ padding:12,
147
+ }}
148
+ >
149
+ <View
150
+ style={{flex:1}}
151
+ >
152
+ <TextInput mode="outlined" label="Edit" textColor={Theme[themeTypeContext].text_color} maxLength={72}
153
+ autoFocus={true}
154
+ right={<TextInput.Affix text={`| Max: 72`} />}
155
+ style={{
156
+ backgroundColor:Theme[themeTypeContext].background_color,
157
+ borderColor:Theme[themeTypeContext].border_color,
158
+
159
+ }}
160
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
161
+ value={editTag}
162
+ onChangeText={(text)=>{
163
+ setEditTag(text)
164
+ }}
165
+ />
166
+ </View>
167
+ <View
168
+ style={{
169
+ display:"flex",
170
+ flexDirection:"row",
171
+ gap:8,
172
+ }}
173
+ >
174
+ <TouchableRipple
175
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
176
+ style={{
177
+ borderRadius:5,
178
+ borderWidth:0,
179
+ backgroundColor: "transparent",
180
+ padding:5,
181
+ }}
182
+
183
+ onPress={()=>{
184
+ setManageBookmark({...manageBookmark,edit:""})
185
+ setEditTag("")
186
+ setShowMenuOption({...showMenuOption,state:false,id:""})
187
+ }}
188
+ >
189
+
190
+ <Icon source={"close"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"red"}/>
191
+ </TouchableRipple>
192
+ <TouchableRipple
193
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
194
+ style={{
195
+ borderRadius:5,
196
+ borderWidth:0,
197
+ backgroundColor: "transparent",
198
+ padding:5,
199
+ }}
200
+
201
+ onPress={async ()=>{
202
+ const stored_bookmark = await Storage.get("bookmark");
203
+
204
+ const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit);
205
+
206
+ if (index !== -1){
207
+ stored_bookmark[index] = editTag;
208
+ await Storage.store("bookmark", stored_bookmark)
209
+
210
+ const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit)
211
+ for (const item of stored_comics){
212
+ await ComicStorage.replaceTag(item.source, item.id, editTag)
213
+ }
214
+
215
+
216
+
217
+
218
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit);
219
+ if (index_2 !== -1){
220
+ BOOKMARK_DATA[index_2].label = editTag
221
+ BOOKMARK_DATA[index_2].value = editTag
222
+ }
223
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
224
+ setManageBookmark({...manageBookmark,edit:""})
225
+ setEditTag("")
226
+
227
+ onRefresh();
228
+ }
229
+ setShowMenuOption({...showMenuOption,state:false,id:""})
230
+ }}
231
+ >
232
+
233
+ <Icon source={"check"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"green"}/>
234
+ </TouchableRipple>
235
+ </View>
236
+
237
+
238
+
239
+ </View>)
240
+
241
+ }</>
242
+
243
+
244
+
245
+
246
+ </View>
247
+
248
+ )
249
+ }
250
+ </>)
251
+ }
252
+ ,[Theme,themeTypeContext,manageBookmark,searchTag,removeTag,createTag,showMenuOption,MIGRATE_BOOKMARK_DATA,BOOKMARK_DATA])
253
+
254
+
255
+ const load_bookmark = async ()=>{
256
+ const stored_bookmark_data = await Storage.get("bookmark") || []
257
+ if (stored_bookmark_data.length) {
258
+ const bookmark_data:Array<Object> = []
259
+ for (const item of stored_bookmark_data) {
260
+ bookmark_data.push({
261
+ label:item,
262
+ value:item,
263
+ })
264
+ }
265
+
266
+ SET_BOOKMARK_DATA(bookmark_data.sort())
267
+ }else SET_BOOKMARK_DATA([])
268
+ }
269
+
270
+ useEffect(()=>{
271
+ SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA])
272
+ },[BOOKMARK_DATA])
273
+
274
+ useEffect(()=>{
275
+ load_bookmark()
276
+ return () => controller.abort();
277
+ },[])
278
+
279
+ return (<>{BOOKMARK_DATA !== null && <>
280
+
281
+ <View key={"BookmarkWidget"}
282
+ style={{
283
+ zIndex:10,
284
+ backgroundColor:Theme[themeTypeContext].background_color,
285
+ maxWidth:500,
286
+ width:"100%",
287
+
288
+ borderColor:Theme[themeTypeContext].border_color,
289
+ borderWidth:2,
290
+ borderRadius:8,
291
+ padding:12,
292
+ display:"flex",
293
+ justifyContent:"center",
294
+
295
+ flexDirection:"column",
296
+ gap:12,
297
+ }}
298
+ from={{
299
+ opacity: 0,
300
+ scale: 0.9,
301
+ }}
302
+ animate={{
303
+ opacity: 1,
304
+ scale: 1,
305
+ }}
306
+ exit={{
307
+ opacity: 0,
308
+ scale: 0.5,
309
+ }}
310
+ transition={{
311
+ type: 'timing',
312
+ duration: 500,
313
+ }}
314
+ exitTransition={{
315
+ type: 'timing',
316
+ duration: 250,
317
+ }}
318
+ >
319
+
320
+ <>{!createTag.state && !removeTag.state && <>
321
+ <View
322
+
323
+ style={{
324
+ display:"flex",
325
+ flexDirection:"column",
326
+ gap:18,
327
+ }}
328
+ >
329
+ <>{BOOKMARK_DATA.length
330
+ ? <>
331
+ <View
332
+ style={{flex:1}}
333
+ >
334
+ <TextInput mode="outlined" label="Search" textColor={Theme[themeTypeContext].text_color}
335
+
336
+ style={{
337
+
338
+ backgroundColor:Theme[themeTypeContext].background_color,
339
+ borderColor:Theme[themeTypeContext].border_color,
340
+
341
+ }}
342
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
343
+ value={searchTag}
344
+ onChange={(event)=>{
345
+ setSearchTag(event.nativeEvent.text)
346
+ }}
347
+ />
348
+ </View>
349
+ <View
350
+ style={{
351
+ maxHeight:Dimensions.height*0.7
352
+ }}
353
+ >
354
+ <ScrollView
355
+ contentContainerStyle={{
356
+ display:"flex",
357
+ flexDirection:"column",
358
+ justifyContent:"space-around",
359
+ gap:8,
360
+
361
+ height:"auto",
362
+ paddingVertical:12,
363
+ paddingHorizontal:8,
364
+ }}
365
+ style={{
366
+
367
+ }}
368
+ >
369
+ <>{BOOKMARK_DATA.map((item:any) =>
370
+ (
371
+ <View key={item.value}>
372
+ <RenderTag item={item}/>
373
+ <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].border_color}}/>
374
+ </View>
375
+ )
376
+ )}</>
377
+ </ScrollView>
378
+ </View>
379
+ </>
380
+ : <>
381
+ <Text style={{
382
+ width:"100%",
383
+ textAlign:"center",
384
+ color:Theme[themeTypeContext].text_color,
385
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.045,
386
+ fontFamily:"roboto-bold",
387
+ }}>No tag found</Text>
388
+ </>
389
+
390
+ }</>
391
+
392
+ <View
393
+ style={{
394
+ display:"flex",
395
+ flexDirection:"row",
396
+ width:"100%",
397
+ justifyContent:"space-around",
398
+ alignItems:"center",
399
+ }}
400
+ >
401
+ <Button mode='contained'
402
+ labelStyle={{
403
+ color:Theme[themeTypeContext].text_color,
404
+ fontFamily:"roboto-medium",
405
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
406
+ }}
407
+ style={{backgroundColor:"green",borderRadius:5}}
408
+ onPress={(()=>{
409
+ setCreateTag({state:true,title:""})
410
+ setShowMenuOption({...showMenuOption,state:false,id:""})
411
+ })}
412
+ >+ Create</Button>
413
+ <Button mode='outlined'
414
+ labelStyle={{
415
+ color:Theme[themeTypeContext].text_color,
416
+ fontFamily:"roboto-medium",
417
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
418
+
419
+
420
+ }}
421
+ style={{
422
+
423
+ borderRadius:5,
424
+ borderWidth:2,
425
+ borderColor:Theme[themeTypeContext].border_color
426
+ }}
427
+ onPress={(async ()=>{
428
+ setWidgetContext({state:false,component:<></>})
429
+ })}
430
+ >Done</Button>
431
+ </View>
432
+ </View>
433
+ </>}</>
434
+
435
+ <>{createTag.state &&
436
+ <>
437
+ <View
438
+ style={{
439
+ height:"auto",
440
+ display:"flex",
441
+ flexDirection:"column",
442
+ gap:12,
443
+ }}
444
+ >
445
+ <TextInput mode="outlined" label="Create Tag" textColor={Theme[themeTypeContext].text_color} maxLength={72}
446
+ placeholder="Bookmark Tag"
447
+
448
+ right={<TextInput.Affix text={`| Max: 72`} />}
449
+ style={{
450
+
451
+ backgroundColor:Theme[themeTypeContext].background_color,
452
+ borderColor:Theme[themeTypeContext].border_color,
453
+
454
+ }}
455
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
456
+ value={createTag.title}
457
+ onChange={(event)=>{
458
+ setCreateTag({...createTag,title:event.nativeEvent.text})
459
+ }}
460
+ />
461
+ </View>
462
+ <View
463
+ style={{
464
+ display:"flex",
465
+ flexDirection:"row",
466
+ width:"100%",
467
+ justifyContent:"space-around",
468
+ alignItems:"center",
469
+ }}
470
+ >
471
+ <Button mode='outlined'
472
+ labelStyle={{
473
+ color:Theme[themeTypeContext].text_color,
474
+ fontFamily:"roboto-medium",
475
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
476
+
477
+
478
+ }}
479
+ style={{
480
+
481
+ borderRadius:5,
482
+ borderWidth:2,
483
+ borderColor:Theme[themeTypeContext].border_color
484
+ }}
485
+ onPress={(()=>{
486
+
487
+ setCreateTag({...createTag,state:false})
488
+
489
+ })}
490
+ >Cancel</Button>
491
+ <Button mode='contained'
492
+ labelStyle={{
493
+ color:Theme[themeTypeContext].text_color,
494
+ fontFamily:"roboto-medium",
495
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
496
+ }}
497
+ style={{backgroundColor:"green",borderRadius:5}}
498
+ onPress={(async()=>{
499
+
500
+ const title = createTag.title
501
+ if (!title) return
502
+
503
+ const stored_bookmark_data = await Storage.get("bookmark") || []
504
+ if (stored_bookmark_data.includes(title)){
505
+ Toast.show({
506
+ type: 'error',
507
+ text1: '🔖 Duplicate Bookmark',
508
+ text2: `Tag "${title}" already existed in your bookmark.`,
509
+
510
+ position: "bottom",
511
+ visibilityTime: 5000,
512
+ text1Style:{
513
+ fontFamily:"roboto-bold",
514
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
515
+ },
516
+ text2Style:{
517
+ fontFamily:"roboto-medium",
518
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
519
+
520
+ },
521
+ });
522
+ }else{
523
+ await Storage.store("bookmark", [...stored_bookmark_data,title].sort())
524
+ SET_BOOKMARK_DATA([...BOOKMARK_DATA,
525
+ {label:title,value:title}
526
+ ].sort())
527
+ setCreateTag({state:false,title:""})
528
+ Toast.show({
529
+ type: 'info',
530
+ text1: '🔖 Create Bookmark',
531
+ text2: `Tag "${title}" added to your bookmark.`,
532
+
533
+ position: "bottom",
534
+ visibilityTime: 3000,
535
+ text1Style:{
536
+ fontFamily:"roboto-bold",
537
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
538
+ },
539
+ text2Style:{
540
+ fontFamily:"roboto-medium",
541
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
542
+
543
+ },
544
+ });
545
+ onRefresh()
546
+ }
547
+ })}
548
+ >Add</Button>
549
+ </View>
550
+ </>
551
+ }</>
552
+
553
+ </View>
554
+ <>{showMenuOption.state &&
555
+ <View
556
+ style={{
557
+ display:"flex",
558
+ position:"absolute",
559
+ zIndex:11,
560
+ justifyContent:"space-around",
561
+ flexDirection:"column",
562
+
563
+ backgroundColor:Theme[themeTypeContext].border_color,
564
+ top:showMenuOption.positions[0],
565
+ bottom:showMenuOption.positions[1],
566
+ left:showMenuOption.positions[2],
567
+ right:showMenuOption.positions[3],
568
+
569
+ width:(Dimensions.width+Dimensions.height)/2*0.2,
570
+ height:(Dimensions.width+Dimensions.height)/2*0.1,
571
+
572
+ borderRadius:5,
573
+ borderWidth:2,
574
+ borderColor:Theme[themeTypeContext].background_color
575
+ }}
576
+ >
577
+ <TouchableRipple
578
+
579
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
580
+ style={{
581
+
582
+ borderWidth:0,
583
+ backgroundColor: "transparent",
584
+ padding:5,
585
+ width:"100%",
586
+
587
+ }}
588
+
589
+ onPress={(event)=>{
590
+ setManageBookmark({...manageBookmark,edit:showMenuOption.id})
591
+ setShowMenuOption({...showMenuOption,state:false})
592
+ }}
593
+ >
594
+ <View
595
+ style={{
596
+ display:"flex",
597
+ flexDirection:"row",
598
+ justifyContent:"center",
599
+ alignItems:"center",
600
+ paddingHorizontal:18,
601
+
602
+ }}
603
+ >
604
+ <Icon source={"pencil"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"cyan"}/>
605
+ <View>
606
+ <Text selectable={false}
607
+ style={{
608
+ textAlign:"center",
609
+ color:"cyan",
610
+ fontFamily:"roboto-medium",
611
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
612
+ }}
613
+ >Edit</Text>
614
+ </View>
615
+ </View>
616
+ </TouchableRipple>
617
+ <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].background_color}}/>
618
+ <TouchableRipple
619
+
620
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
621
+ style={{
622
+
623
+ borderWidth:0,
624
+ backgroundColor: "transparent",
625
+ padding:5,
626
+ width:"100%",
627
+ }}
628
+
629
+ onPress={(event)=>{
630
+ setManageBookmark({...manageBookmark,edit:"",delete:showMenuOption.id})
631
+ setShowMenuOption({...showMenuOption,state:false})
632
+ }}
633
+ >
634
+ <View
635
+ style={{
636
+ display:"flex",
637
+ flexDirection:"row",
638
+ justifyContent:"center",
639
+ alignItems:"center",
640
+ paddingHorizontal:18,
641
+
642
+ }}
643
+ >
644
+ <Icon source={"trash-can"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"red"}/>
645
+ <View>
646
+ <Text selectable={false}
647
+ style={{
648
+ textAlign:"center",
649
+ color:"red",
650
+ fontFamily:"roboto-medium",
651
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
652
+ }}
653
+ >Delete</Text>
654
+ </View>
655
+ </View>
656
+ </TouchableRipple>
657
+
658
+ </View>
659
+
660
+ }</>
661
+ <>{manageBookmark.delete && (
662
+
663
+
664
+ <View
665
+ style={{
666
+ top:0,
667
+ left:0,
668
+ position:"absolute",
669
+ width:Dimensions.width,
670
+ height:Dimensions.height,
671
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
672
+ zIndex:11,
673
+ display:"flex",
674
+ justifyContent:"center",
675
+ alignItems:"center",
676
+ padding:15,
677
+ }}
678
+ >
679
+ <View
680
+ style={{
681
+ backgroundColor:Theme[themeTypeContext].background_color,
682
+ maxWidth:500,
683
+ width:"100%",
684
+ height:"auto",
685
+
686
+ borderColor:Theme[themeTypeContext].border_color,
687
+ borderWidth:2,
688
+ borderRadius:8,
689
+ padding:12,
690
+ display:"flex",
691
+ justifyContent:"center",
692
+
693
+ flexDirection:"column",
694
+ gap:18,
695
+ }}
696
+ >
697
+ <View
698
+ style={{
699
+ borderBottomWidth:2,
700
+ borderColor:Theme[themeTypeContext].border_color,
701
+ padding:8,
702
+ width:"100%",
703
+ }}
704
+ >
705
+ <Text
706
+ numberOfLines={1}
707
+ style={{
708
+ color:"red",
709
+ fontFamily:"roboto-bold",
710
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03,
711
+ textAlign:"center",
712
+ }}
713
+ >Delete Tag: "{manageBookmark.delete}"</Text>
714
+ </View>
715
+ <View
716
+ style={{
717
+ width:"100%",
718
+ display:"flex",
719
+ flexDirection:"column",
720
+ gap:12,
721
+ }}
722
+ >
723
+ <View style={{flex:1}}>
724
+ <Dropdown
725
+ theme_type={themeTypeContext}
726
+ Dimensions={Dimensions}
727
+
728
+ label='Migrate comics to tag'
729
+ data={MIGRATE_BOOKMARK_DATA.filter((item:any) => item.value !== manageBookmark.delete)}
730
+ value={migrateTag}
731
+ onChange={(async (item:any) => {
732
+ setMigrateTag(item.value)
733
+ })}
734
+ />
735
+ </View>
736
+ <>{!migrateTag && (
737
+ <Text
738
+ style={{
739
+ color:Theme[themeTypeContext].text_color,
740
+ fontFamily:"roboto-bold",
741
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
742
+ textAlign:"center",
743
+ }}
744
+ >Setting migration to None will remove all comics and chapters for this bookmark tag.</Text>
745
+ )}</>
746
+ <View
747
+ style={{
748
+ display:"flex",
749
+ flexDirection:"row",
750
+ width:"100%",
751
+ justifyContent:"space-around",
752
+ alignItems:"center",
753
+ }}
754
+ >
755
+ <>{migrateTag
756
+
757
+ ? <TouchableRipple
758
+
759
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
760
+ style={{
761
+
762
+ borderWidth:0,
763
+ backgroundColor: "blue",
764
+ padding:5,
765
+ borderRadius:8,
766
+ paddingHorizontal:12,
767
+ paddingVertical:8,
768
+
769
+
770
+ }}
771
+
772
+ onPress={async (event)=>{
773
+ const stored_bookmark = await Storage.get("bookmark")
774
+ const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete)
775
+ if (index === -1) return
776
+
777
+ const stored_comics = await ComicStorage.getByTag(manageBookmark.delete)
778
+ for (const comic of stored_comics) {
779
+ const source = comic.source;
780
+ const comic_id = comic.id
781
+ await ComicStorage.replaceTag(source,comic_id,migrateTag)
782
+
783
+ }
784
+
785
+ stored_bookmark.splice(index, 1);
786
+ await Storage.store("bookmark",stored_bookmark);
787
+
788
+
789
+
790
+
791
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete);
792
+ if (index_2 !== -1){
793
+ BOOKMARK_DATA.splice(index_2, 1);
794
+ }
795
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
796
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
797
+ setShowMenuOption({...showMenuOption,state:false,id:""})
798
+ setMigrateTag("")
799
+
800
+ onRefresh();
801
+ }}
802
+ >
803
+
804
+ <Text selectable={false}
805
+ style={{
806
+ textAlign:"center",
807
+ color:Theme[themeTypeContext].text_color,
808
+ fontFamily:"roboto-medium",
809
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
810
+ }}
811
+ >Migrate</Text>
812
+
813
+ </TouchableRipple>
814
+ : <TouchableRipple
815
+
816
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
817
+ style={{
818
+
819
+ borderWidth:0,
820
+ backgroundColor: "red",
821
+ padding:5,
822
+ borderRadius:8,
823
+ paddingHorizontal:12,
824
+ paddingVertical:8,
825
+
826
+
827
+ }}
828
+
829
+ onPress={async (event)=>{
830
+ const stored_bookmark = await Storage.get("bookmark");
831
+ const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete)
832
+ if (index === -1) return
833
+ await ComicStorage.removeByTag(manageBookmark.delete);
834
+ stored_bookmark.splice(index, 1);
835
+ await Storage.store("bookmark",stored_bookmark);
836
+
837
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete);
838
+ if (index_2 !== -1){
839
+ BOOKMARK_DATA.splice(index_2, 1);
840
+ }
841
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
842
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
843
+ setShowMenuOption({...showMenuOption,state:false,id:""})
844
+ setMigrateTag("")
845
+ onRefresh()
846
+
847
+ }}
848
+ >
849
+
850
+ <Text selectable={false}
851
+ style={{
852
+ textAlign:"center",
853
+ color:Theme[themeTypeContext].text_color,
854
+ fontFamily:"roboto-medium",
855
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
856
+ }}
857
+ >Delete</Text>
858
+
859
+ </TouchableRipple>
860
+ }</>
861
+ <TouchableRipple
862
+
863
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
864
+ style={{
865
+
866
+ borderWidth:2,
867
+ borderColor:Theme[themeTypeContext].border_color,
868
+ backgroundColor: "transparent",
869
+ padding:5,
870
+ borderRadius:8,
871
+ paddingHorizontal:12,
872
+ paddingVertical:8,
873
+
874
+
875
+ }}
876
+
877
+ onPress={(event)=>{
878
+ setShowMenuOption({...showMenuOption,state:false,id:""})
879
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
880
+ }}
881
+ >
882
+
883
+ <Text selectable={false}
884
+ style={{
885
+ textAlign:"center",
886
+ color:Theme[themeTypeContext].text_color,
887
+ fontFamily:"roboto-medium",
888
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
889
+ }}
890
+ >Cancel</Text>
891
+
892
+ </TouchableRipple>
893
+ </View>
894
+
895
+ </View>
896
+ </View>
897
+
898
+ </View>
899
+ )}</>
900
+
901
+ </>}</>)
902
+ }
903
+
904
+ export default BookmarkWidget;
frontend/app/recent/index.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react';
2
+ import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router';
3
+ import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper';
6
+ import CircularProgress from 'react-native-circular-progress-indicator';
7
+ import { ActivityIndicator } from 'react-native-paper';
8
+ import { FlashList } from "@shopify/flash-list";
9
+
10
+
11
+ import uuid from 'react-native-uuid';
12
+ import Toast from 'react-native-toast-message';
13
+ import { View, AnimatePresence } from 'moti';
14
+ import * as Clipboard from 'expo-clipboard';
15
+ import * as FileSystem from 'expo-file-system';
16
+ import NetInfo from "@react-native-community/netinfo";
17
+ import { Marquee } from '@animatereactnative/marquee';
18
+ import { Slider } from '@rneui/themed-edge';
19
+
20
+
21
+ import ComicComponent from './components/comic_component';
22
+ import BookmarkWidget from './components/widgets/bookmark';
23
+
24
+ import { __styles } from './stylesheet/styles';
25
+ import Storage from '@/constants/module/storages/storage';
26
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
27
+ import Image from '@/components/Image';
28
+ import {CONTEXT} from '@/constants/module/context';
29
+ import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager";
30
+ import Theme from '@/constants/theme';
31
+ import ComicStorage from '@/constants/module/storages/comic_storage';
32
+
33
+ const Index = ({}:any) => {
34
+ const Dimensions = useWindowDimensions();
35
+ const controller = new AbortController();
36
+ const signal = controller.signal;
37
+
38
+ const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
39
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
40
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
41
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
42
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
43
+
44
+ const [styles, setStyles]:any = useState("")
45
+ const [isLoading, setIsLoading] = useState<boolean>(true)
46
+ const [onRefresh, setOnRefresh] = useState(false)
47
+
48
+
49
+ const [COMIC_DATA, SET_COMIC_DATA] = useState<any>([])
50
+
51
+
52
+ useFocusEffect(useCallback(() => {
53
+ (async ()=>{
54
+ setIsLoading(true)
55
+ const stored_recent = await Storage.get("RECENT") || []
56
+ stored_recent.sort((a:any,b:any) => b.timestamp - a.timestamp)
57
+ const stored_comic = []
58
+ for (const item of stored_recent) {
59
+ const comic = await ComicStorage.getByID(item.source,item.comic_id)
60
+ if (comic) stored_comic.push(comic)
61
+ }
62
+ SET_COMIC_DATA(stored_comic)
63
+ setIsLoading(false)
64
+ })()
65
+ },[onRefresh]))
66
+
67
+
68
+
69
+ const RenderComicComponent = useCallback(({item,index}:any) => {
70
+ console.log(item,index)
71
+ return <ComicComponent index={index} item={item}
72
+ />
73
+ },[])
74
+
75
+ useFocusEffect(useCallback(() => {
76
+ setIsLoading(true)
77
+ setShowMenuContext(true)
78
+ setStyles(__styles(themeTypeContext,Dimensions))
79
+
80
+ return () => {
81
+ controller.abort();
82
+ };
83
+ },[]))
84
+
85
+ return (<>{styles && ! isLoading
86
+ ? <>
87
+ <View style={styles.screen_container}
88
+
89
+ >
90
+ <View style={styles.header_container}>
91
+ <Text style={styles.header_text}>Recent</Text>
92
+ </View>
93
+ <ScrollView
94
+ contentContainerStyle={{
95
+ width:"100%",
96
+ height:COMIC_DATA.length ? "auto" : "100%",
97
+ maxHeight:"auto",
98
+ display:"flex",
99
+ paddingHorizontal:12,
100
+ paddingVertical:18,
101
+ flexDirection:"row",
102
+ justifyContent:"flex-start",
103
+ gap:Math.max((Dimensions.width+Dimensions.height)/2*0.015,8),
104
+ flexWrap:"wrap",
105
+ }}
106
+
107
+ >
108
+ <>{COMIC_DATA.length
109
+ ? <>{COMIC_DATA.map((item:any,index:number) => (
110
+ <RenderComicComponent key={index} item={item} index={index} />
111
+ ))
112
+ }</>
113
+ : <View
114
+ style={{
115
+ width:"100%",
116
+ height:"100%",
117
+ backgroundColor:"transparent",
118
+ display:"flex",
119
+ justifyContent:"center",
120
+ alignItems:"center",
121
+ flexDirection:"row",
122
+ gap:12,
123
+ }}
124
+ >
125
+ <>
126
+ <Icon source={"dots-hexagon"} color={Theme[themeTypeContext].icon_color} size={((Dimensions.width+Dimensions.height)/2)*0.03}/>
127
+ <Text selectable={false}
128
+ style={{
129
+ fontFamily:"roboto-bold",
130
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
131
+ color:Theme[themeTypeContext].text_color,
132
+ }}
133
+ >There no recent read.</Text>
134
+ </>
135
+ </View>
136
+ }</>
137
+
138
+ </ScrollView>
139
+ </View>
140
+
141
+ </>
142
+ : <View style={{zIndex:5,width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",backgroundColor:Theme[themeTypeContext].background_color}}>
143
+ <Image setShowCloudflareTurnstile={setShowCloudflareTurnstileContext} source={require("@/assets/gif/cat-loading.gif")} style={{width:((Dimensions.width+Dimensions.height)/2)*0.15,height:((Dimensions.width+Dimensions.height)/2)*0.15}}/>
144
+ </View>
145
+ }</>)
146
+
147
+
148
+ }
149
+
150
+ export default Index;
151
+
frontend/app/recent/stylesheet/styles.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StyleSheet } from "react-native";
2
+ import Theme from "@/constants/theme";
3
+
4
+ export const __styles:any = (theme_type:string,Dimensions:any) => {
5
+ return StyleSheet.create({
6
+ screen_container: {
7
+ display: "flex",
8
+ width: "100%",
9
+ height: "100%",
10
+ backgroundColor: Theme[theme_type].background_color,
11
+ },
12
+ header_container: {
13
+ display: "flex",
14
+ flexDirection: "row",
15
+ justifyContent: "space-between",
16
+ alignItems: "center",
17
+ paddingHorizontal: 15,
18
+ paddingVertical:10,
19
+ backgroundColor: Theme[theme_type].background_color,
20
+ borderBottomWidth: 0.5,
21
+ borderColor: Theme[theme_type].border_color,
22
+ },
23
+ header_text:{
24
+ fontFamily: "roboto-medium",
25
+ fontSize: ((Dimensions.width+Dimensions.height)/2)*0.04,
26
+ color: Theme[theme_type].text_color,
27
+
28
+ },
29
+
30
+ item_box:{
31
+ display:"flex",
32
+ flexDirection:"column",
33
+ alignItems:"center",
34
+ gap:15,
35
+ height:"auto",
36
+ width:Math.max(((Dimensions.width+Dimensions.height)/2)*0.225,100),
37
+ borderRadius:8,
38
+
39
+ },
40
+ item_cover:{
41
+ width:"100%",
42
+ height:Math.max(((Dimensions.width+Dimensions.height)/2)*0.325,125),
43
+ borderRadius:8,
44
+ shadowColor: "#000",
45
+ shadowOffset: {
46
+ width: 0,
47
+ height: 1,
48
+ },
49
+ shadowOpacity: 0.22,
50
+ shadowRadius: 2.22,
51
+
52
+ elevation: 3,
53
+ },
54
+ item_title:{
55
+ color: Theme[theme_type].text_color,
56
+ fontFamily: "roboto-medium",
57
+ fontSize: ((Dimensions.width+Dimensions.height)/2)*0.025,
58
+ width:"100%",
59
+ height:"auto",
60
+ textAlign:"center",
61
+ flexShrink:1,
62
+ }
63
+ })}
frontend/app/view/[source]/[comic_id].tsx CHANGED
@@ -12,6 +12,8 @@ import Toast from 'react-native-toast-message';
12
  import { View, AnimatePresence } from 'moti';
13
  import * as Clipboard from 'expo-clipboard';
14
  import NetInfo from "@react-native-community/netinfo";
 
 
15
 
16
 
17
  import Theme from '@/constants/theme';
@@ -20,6 +22,8 @@ import Storage from '@/constants/module/storages/storage';
20
  import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
21
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
22
  import ComicStorage from '@/constants/module/storages/comic_storage';
 
 
23
  import { CONTEXT } from '@/constants/module/context';
24
  import Dropdown from '@/components/dropdown';
25
  import PageNavigationWidget from '../componenets/widgets/page_navigation';
@@ -39,6 +43,8 @@ import { createSocket, setupSocketNetworkListener } from '../modules/socket';
39
  const Index = ({}:any) => {
40
  const SOURCE = useLocalSearchParams().source;
41
  const ID = useLocalSearchParams().comic_id;
 
 
42
 
43
  const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
44
  const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
@@ -61,10 +67,9 @@ const Index = ({}:any) => {
61
 
62
 
63
  const [CONTENT, SET_CONTENT]:any = useState({})
64
- const [chapterRequested, setChapterRequested]:any = useState({})
65
- const [chapterToDownload, setChapterToDownload]:any = useState({})
66
- const [downloadProgress, setDownloadProgress]:any = useState(0)
67
- const [chapterQueue, setChapterQueue]:any = useState({})
68
  const [isLoading, setIsLoading]:any = useState(true);
69
  const [feedBack, setFeedBack]:any = useState("");
70
  const [showOption, setShowOption]:any = useState({type:null})
@@ -77,6 +82,11 @@ const Index = ({}:any) => {
77
 
78
  const socketNetWorkListener:any = useRef(null)
79
  const socket:any = useRef(null)
 
 
 
 
 
80
 
81
  const controller = new AbortController();
82
  const signal = controller.signal;
@@ -87,30 +97,40 @@ const Index = ({}:any) => {
87
 
88
  },[CONTENT])
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  // Worker for downloading chapter
92
  const download_chapter_interval:any = useRef(null)
93
  const isDownloading:any = useRef(false)
94
- useEffect(() => {
95
  clearInterval(download_chapter_interval.current)
96
-
97
  download_chapter_interval.current = setInterval(() => {
98
- if (!isDownloading.current && Object.keys(chapterToDownload).length){
99
  isDownloading.current = true
100
- console.log(isDownloading.current,chapterToDownload)
101
- console.log("Downloading HERE")
102
  download_chapter(
103
  setShowCloudflareTurnstileContext, isDownloading, SOURCE, ID,
104
- chapterRequested, setChapterRequested,
105
- chapterToDownload, setChapterToDownload,
106
- downloadProgress, setDownloadProgress,
107
- signal,
108
  )
109
  }
110
  },1000)
111
 
112
  return () => clearInterval(download_chapter_interval.current)
113
- },[chapterToDownload])
114
 
115
  // Setting up socket listener
116
  useFocusEffect(useCallback(() => {
@@ -130,10 +150,9 @@ const Index = ({}:any) => {
130
  if (!stored_comic) return
131
  const event = result.event
132
  if (event.type === "chapter_queue_info"){
133
- console.log(event.chapter_queue)
134
- setChapterQueue(event.chapter_queue)
135
  }else if (event.type === "chapter_ready_to_download"){
136
- get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID)
137
  }
138
  }
139
  }
@@ -166,11 +185,11 @@ const Index = ({}:any) => {
166
  },[]))
167
 
168
 
169
- const Load_Offline = async () => {
170
  Toast.show({
171
  type: 'info',
172
- text1: '🌐 No internet connection available.',
173
- text2: `Switching to offline mode.`,
174
 
175
  position: "bottom",
176
  visibilityTime: 6000,
@@ -191,6 +210,7 @@ const Index = ({}:any) => {
191
  if (stored_comic) {
192
  const DATA:any = {}
193
  DATA["id"] = ID
 
194
 
195
  for (const [key, value] of Object.entries(stored_comic.info)) {
196
  DATA[key] = value
@@ -244,18 +264,18 @@ const Index = ({}:any) => {
244
 
245
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
246
  if (stored_comic) {
247
- await get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID)
248
-
249
  setBookmarked({state:true,tag:stored_comic.tag})
250
  setHistory(stored_comic.history)
251
  }
252
  else setBookmarked({state:false,tag:""})
253
 
254
  const net_info = await NetInfo.fetch()
255
- if (net_info.isConnected){
 
256
  get(setShowCloudflareTurnstileContext, setIsLoading, signal, __translate, setFeedBack, SOURCE, ID, SET_CONTENT)
257
  }else{
258
- Load_Offline()
 
259
  }
260
 
261
  })()
@@ -280,11 +300,10 @@ const Index = ({}:any) => {
280
  if (net_info.isConnected){
281
  get(setShowCloudflareTurnstileContext, setIsLoading, signal, translate, setFeedBack, SOURCE, ID, SET_CONTENT)
282
  if (stored_comic) {
283
- setHistory(stored_comic.history)
284
- get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID)
285
  }
286
  }else{
287
- Load_Offline()
288
  }
289
  }
290
 
@@ -312,7 +331,7 @@ const Index = ({}:any) => {
312
 
313
  onPress={()=>{
314
  if (router.canGoBack()) router.back()
315
- else router.replace("/explore")
316
  }}
317
  >
318
  <Icon source={"arrow-left-thin"} size={((Dimensions.width+Dimensions.height)/2)*0.045} color={Theme[themeTypeContext].icon_color}/>
@@ -354,7 +373,7 @@ const Index = ({}:any) => {
354
  onRefresh()
355
  }}
356
  >
357
- <Icon source={"refresh"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={Theme[themeTypeContext].icon_color}/>
358
  </TouchableRipple>
359
  <TouchableRipple
360
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
@@ -370,7 +389,7 @@ const Index = ({}:any) => {
370
  Toast.show({
371
  type: 'info',
372
  text1: '📋 Copied to your clipboard.',
373
- text2: `${apiBaseContext}/view/${SOURCE}/${ID}/`,
374
 
375
  position: "bottom",
376
  visibilityTime: 3000,
@@ -583,6 +602,7 @@ const Index = ({}:any) => {
583
  onPress={()=>{
584
  setWidgetContext({state:true,component:
585
  <BookmarkWidget
 
586
  onRefresh={onRefresh}
587
  SOURCE={SOURCE}
588
  ID={ID}
@@ -645,7 +665,7 @@ const Index = ({}:any) => {
645
 
646
  }}
647
  onPress={()=>{
648
- router.push(`/read/${SOURCE}/${ID}/${history.idx}/`)
649
  }}
650
  >
651
  <View
@@ -679,44 +699,63 @@ const Index = ({}:any) => {
679
  </View>
680
  </TouchableRipple>
681
  : <TouchableRipple
682
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
683
- style={{
684
- width:Dimensions.width*0.60,
685
- display:"flex",
686
- flexDirection:"column",
687
- justifyContent:"center",
688
- alignSelf:"center",
689
- padding:8,
690
- paddingVertical:12,
691
- borderRadius:Dimensions.width*0.60/2,
692
- backgroundColor:Theme[themeTypeContext].border_color,
693
-
694
- shadowColor: Theme[themeTypeContext].shadow_color,
695
- shadowOffset: { width: 0, height: 2 },
696
- shadowOpacity: 0.25,
697
- shadowRadius: 3.84,
698
- elevation: 5,
699
-
700
- }}
701
- onPress={async ()=>{
702
-
703
- if (bookmarked.state){
704
- let chapter
705
- if (sort === "descending"){
706
- chapter = CONTENT?.chapters[CONTENT.chapters.length - 1]
707
- }else{
708
- chapter = CONTENT?.chapters[0]
709
- }
710
- const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id)
711
- console.log(stored_chapter)
712
- if (stored_chapter.data_state === "completed"){
713
- await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
714
- router.push(`/read/${SOURCE}/${ID}/${stored_chapter.idx}/`)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
715
  }else{
716
  Toast.show({
717
  type: 'error',
718
- text1: 'Chapter not download yet.',
719
- text2: "Press the button next to chapter title to download.",
720
 
721
  position: "bottom",
722
  visibilityTime: 4000,
@@ -730,30 +769,11 @@ const Index = ({}:any) => {
730
 
731
  },
732
  });
733
- }
734
- }else{
735
- Toast.show({
736
- type: 'error',
737
- text1: '🔖 Bookmark required.',
738
- text2: `Add this comic to your bookmark to start reading.`,
739
-
740
- position: "bottom",
741
- visibilityTime: 4000,
742
- text1Style:{
743
- fontFamily:"roboto-bold",
744
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
745
- },
746
- text2Style:{
747
- fontFamily:"roboto-medium",
748
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
749
-
750
- },
751
- });
752
-
753
 
754
- }
755
- }}
756
- >
 
757
  <View
758
  style={{
759
  display:"flex",
@@ -796,19 +816,31 @@ const Index = ({}:any) => {
796
  >Synopsis:</Text>
797
  </View>
798
 
799
- <Button mode='outlined'
 
800
  onPress={() => {
801
  if (showMoreSynopsis) setShowMoreSynopsis(false)
802
  else setShowMoreSynopsis(true)
803
  }}
804
- style={{borderWidth:0}}
805
- labelStyle={{
806
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
807
- fontFamily:"roboto-medium",
808
- color:"cyan",
809
-
 
810
  }}
811
- >{showMoreSynopsis ? "Show Less" : "Show More"}</Button>
 
 
 
 
 
 
 
 
 
 
812
 
813
  </View>
814
 
@@ -821,6 +853,7 @@ const Index = ({}:any) => {
821
  borderColor: Theme[themeTypeContext].border_color,
822
  borderBottomWidth:showMoreSynopsis ? 0 : 5,
823
  borderRadius:8,
 
824
  }}
825
  numberOfLines={showMoreSynopsis ? 0 : 2}
826
  ellipsizeMode='tail'
@@ -870,24 +903,7 @@ const Index = ({}:any) => {
870
  <View style={styles.chapter_box}>
871
  <>{CONTENT.chapters.length
872
  ? <>{CONTENT.chapters.slice((page-1)*MAX_OFFSET,((page-1)*MAX_OFFSET)+MAX_OFFSET).map((chapter:any,index:number) =>
873
- <ChapterComponent
874
- key={index}
875
- SOURCE={SOURCE}
876
- ID={ID}
877
- page={page}
878
- sort={sort}
879
- chapter={chapter}
880
- signal={signal}
881
- isDownloading={isDownloading}
882
- chapterRequested={chapterRequested}
883
- setChapterRequested={setChapterRequested}
884
- chapterToDownload={chapterToDownload}
885
- setChapterToDownload={setChapterToDownload}
886
- downloadProgress={downloadProgress}
887
- setDownloadProgress={setDownloadProgress}
888
- setChapterQueue={setChapterQueue}
889
- chapterQueue={chapterQueue}
890
- />
891
  )}</>
892
  : <Text
893
  style={{
 
12
  import { View, AnimatePresence } from 'moti';
13
  import * as Clipboard from 'expo-clipboard';
14
  import NetInfo from "@react-native-community/netinfo";
15
+ import _ from 'lodash'
16
+
17
 
18
 
19
  import Theme from '@/constants/theme';
 
22
  import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
23
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
24
  import ComicStorage from '@/constants/module/storages/comic_storage';
25
+ import CoverStorage from '@/constants/module/storages/cover_storage';
26
+
27
  import { CONTEXT } from '@/constants/module/context';
28
  import Dropdown from '@/components/dropdown';
29
  import PageNavigationWidget from '../componenets/widgets/page_navigation';
 
43
  const Index = ({}:any) => {
44
  const SOURCE = useLocalSearchParams().source;
45
  const ID = useLocalSearchParams().comic_id;
46
+ const MODE = useLocalSearchParams().mode;
47
+ console.log(MODE)
48
 
49
  const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
50
  const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
 
67
 
68
 
69
  const [CONTENT, SET_CONTENT]:any = useState({})
70
+
71
+
72
+
 
73
  const [isLoading, setIsLoading]:any = useState(true);
74
  const [feedBack, setFeedBack]:any = useState("");
75
  const [showOption, setShowOption]:any = useState({type:null})
 
82
 
83
  const socketNetWorkListener:any = useRef(null)
84
  const socket:any = useRef(null)
85
+
86
+ const download_progress:any = useRef({})
87
+ const chapter_queue:any = useRef({})
88
+ const chapter_requested:any = useRef({})
89
+ const chapter_to_download:any = useRef({})
90
 
91
  const controller = new AbortController();
92
  const signal = controller.signal;
 
97
 
98
  },[CONTENT])
99
 
100
+ const RenderChapter = useCallback(({chapter}:any) => {
101
+ return <ChapterComponent
102
+ SOURCE={SOURCE}
103
+ ID={ID}
104
+ page={page}
105
+ sort={sort}
106
+ chapter={chapter}
107
+ signal={signal}
108
+ isDownloading={isDownloading}
109
+ chapter_requested={chapter_requested}
110
+ chapter_to_download={chapter_to_download}
111
+ download_progress={download_progress}
112
+ chapter_queue={chapter_queue}
113
+ />
114
+ },[page,sort])
115
+
116
 
117
  // Worker for downloading chapter
118
  const download_chapter_interval:any = useRef(null)
119
  const isDownloading:any = useRef(false)
120
+ useFocusEffect(useCallback(() => {
121
  clearInterval(download_chapter_interval.current)
 
122
  download_chapter_interval.current = setInterval(() => {
123
+ if (!isDownloading.current && Object.keys(chapter_to_download.current).length){
124
  isDownloading.current = true
 
 
125
  download_chapter(
126
  setShowCloudflareTurnstileContext, isDownloading, SOURCE, ID,
127
+ chapter_requested, chapter_to_download, download_progress, signal,
 
 
 
128
  )
129
  }
130
  },1000)
131
 
132
  return () => clearInterval(download_chapter_interval.current)
133
+ },[]))
134
 
135
  // Setting up socket listener
136
  useFocusEffect(useCallback(() => {
 
150
  if (!stored_comic) return
151
  const event = result.event
152
  if (event.type === "chapter_queue_info"){
153
+ chapter_queue.current = event.chapter_queue;
 
154
  }else if (event.type === "chapter_ready_to_download"){
155
+ get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID)
156
  }
157
  }
158
  }
 
185
  },[]))
186
 
187
 
188
+ const Load_Local = async () => {
189
  Toast.show({
190
  type: 'info',
191
+ text1: '💾 Local mode',
192
+ text2: `Press refresh button to fetch new updates.`,
193
 
194
  position: "bottom",
195
  visibilityTime: 6000,
 
210
  if (stored_comic) {
211
  const DATA:any = {}
212
  DATA["id"] = ID
213
+ DATA["cover"] = await CoverStorage.get(`${SOURCE}-${ID}`)
214
 
215
  for (const [key, value] of Object.entries(stored_comic.info)) {
216
  DATA[key] = value
 
264
 
265
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
266
  if (stored_comic) {
 
 
267
  setBookmarked({state:true,tag:stored_comic.tag})
268
  setHistory(stored_comic.history)
269
  }
270
  else setBookmarked({state:false,tag:""})
271
 
272
  const net_info = await NetInfo.fetch()
273
+ if (net_info.isConnected && MODE !== "local"){
274
+ if (stored_comic) await get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID)
275
  get(setShowCloudflareTurnstileContext, setIsLoading, signal, __translate, setFeedBack, SOURCE, ID, SET_CONTENT)
276
  }else{
277
+ if (net_info.isConnected && stored_comic) await get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID)
278
+ Load_Local()
279
  }
280
 
281
  })()
 
300
  if (net_info.isConnected){
301
  get(setShowCloudflareTurnstileContext, setIsLoading, signal, translate, setFeedBack, SOURCE, ID, SET_CONTENT)
302
  if (stored_comic) {
303
+ get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID)
 
304
  }
305
  }else{
306
+ Load_Local()
307
  }
308
  }
309
 
 
331
 
332
  onPress={()=>{
333
  if (router.canGoBack()) router.back()
334
+ else router.replace("/bookmark")
335
  }}
336
  >
337
  <Icon source={"arrow-left-thin"} size={((Dimensions.width+Dimensions.height)/2)*0.045} color={Theme[themeTypeContext].icon_color}/>
 
373
  onRefresh()
374
  }}
375
  >
376
+ <Icon source={"update"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={Theme[themeTypeContext].icon_color}/>
377
  </TouchableRipple>
378
  <TouchableRipple
379
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
 
389
  Toast.show({
390
  type: 'info',
391
  text1: '📋 Copied to your clipboard.',
392
+ text2: `https://comicmtl.netlify.app/view/${SOURCE}/${ID}/`,
393
 
394
  position: "bottom",
395
  visibilityTime: 3000,
 
602
  onPress={()=>{
603
  setWidgetContext({state:true,component:
604
  <BookmarkWidget
605
+ setIsLoading={setIsLoading}
606
  onRefresh={onRefresh}
607
  SOURCE={SOURCE}
608
  ID={ID}
 
665
 
666
  }}
667
  onPress={()=>{
668
+ router.replace(`/read/${SOURCE}/${ID}/${history.idx}/`)
669
  }}
670
  >
671
  <View
 
699
  </View>
700
  </TouchableRipple>
701
  : <TouchableRipple
702
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
703
+ style={{
704
+ width:Dimensions.width*0.60,
705
+ display:"flex",
706
+ flexDirection:"column",
707
+ justifyContent:"center",
708
+ alignSelf:"center",
709
+ padding:8,
710
+ paddingVertical:12,
711
+ borderRadius:Dimensions.width*0.60/2,
712
+ backgroundColor:Theme[themeTypeContext].border_color,
713
+
714
+ shadowColor: Theme[themeTypeContext].shadow_color,
715
+ shadowOffset: { width: 0, height: 2 },
716
+ shadowOpacity: 0.25,
717
+ shadowRadius: 3.84,
718
+ elevation: 5,
719
+
720
+ }}
721
+ onPress={async ()=>{
722
+
723
+ if (bookmarked.state){
724
+ let chapter
725
+ if (sort === "descending"){
726
+ chapter = CONTENT?.chapters[CONTENT.chapters.length - 1]
727
+ }else{
728
+ chapter = CONTENT?.chapters[0]
729
+ }
730
+ const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id)
731
+ if (stored_chapter.data_state === "completed"){
732
+ await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
733
+ router.replace(`/read/${SOURCE}/${ID}/${stored_chapter.idx}/`)
734
+
735
+ }else{
736
+ Toast.show({
737
+ type: 'error',
738
+ text1: 'Chapter not download yet.',
739
+ text2: "Press the button next to chapter title to download.",
740
+
741
+ position: "bottom",
742
+ visibilityTime: 4000,
743
+ text1Style:{
744
+ fontFamily:"roboto-bold",
745
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
746
+ },
747
+ text2Style:{
748
+ fontFamily:"roboto-medium",
749
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
750
+
751
+ },
752
+ });
753
+ }
754
  }else{
755
  Toast.show({
756
  type: 'error',
757
+ text1: '🔖 Bookmark required.',
758
+ text2: `Add this comic to your bookmark to start reading.`,
759
 
760
  position: "bottom",
761
  visibilityTime: 4000,
 
769
 
770
  },
771
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
 
773
+
774
+ }
775
+ }}
776
+ >
777
  <View
778
  style={{
779
  display:"flex",
 
816
  >Synopsis:</Text>
817
  </View>
818
 
819
+ <TouchableRipple
820
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
821
  onPress={() => {
822
  if (showMoreSynopsis) setShowMoreSynopsis(false)
823
  else setShowMoreSynopsis(true)
824
  }}
825
+ style={{
826
+ borderWidth:0,
827
+ display:"flex",
828
+ justifyContent:"center",
829
+ borderRadius:((Dimensions.width+Dimensions.height)/2)*0.015,
830
+ paddingHorizontal:12,
831
+ backgroundColor:"transparent",
832
  }}
833
+ >
834
+ <Text selectable={false}
835
+ style={{
836
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
837
+ fontFamily:"roboto-medium",
838
+ color:"cyan",
839
+ textAlign:"center",
840
+ textDecorationLine: showMoreSynopsis ? "underline" : "none",
841
+ }}
842
+ >{showMoreSynopsis ? "Show Less" : "Show More"}</Text>
843
+ </TouchableRipple>
844
 
845
  </View>
846
 
 
853
  borderColor: Theme[themeTypeContext].border_color,
854
  borderBottomWidth:showMoreSynopsis ? 0 : 5,
855
  borderRadius:8,
856
+
857
  }}
858
  numberOfLines={showMoreSynopsis ? 0 : 2}
859
  ellipsizeMode='tail'
 
903
  <View style={styles.chapter_box}>
904
  <>{CONTENT.chapters.length
905
  ? <>{CONTENT.chapters.slice((page-1)*MAX_OFFSET,((page-1)*MAX_OFFSET)+MAX_OFFSET).map((chapter:any,index:number) =>
906
+ <RenderChapter key={index} chapter={chapter}/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  )}</>
908
  : <Text
909
  style={{
frontend/app/view/componenets/chapter.tsx CHANGED
@@ -12,6 +12,7 @@ import Toast from 'react-native-toast-message';
12
  import { View, AnimatePresence } from 'moti';
13
  import * as Clipboard from 'expo-clipboard';
14
  import NetInfo from "@react-native-community/netinfo";
 
15
 
16
 
17
  import Theme from '@/constants/theme';
@@ -36,14 +37,10 @@ const ChapterComponent = ({
36
  chapter,
37
  signal,
38
  isDownloading,
39
- chapterRequested,
40
- setChapterRequested,
41
- downloadProgress,
42
- setDownloadProgress,
43
- chapterToDownload,
44
- setChapterToDownload,
45
- setChapterQueue,
46
- chapterQueue,
47
 
48
  }:any) => {
49
  const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
@@ -57,9 +54,87 @@ const ChapterComponent = ({
57
 
58
  const [styles, setStyles]:any = useState("")
59
  const [is_saved, set_is_saved] = useState(false)
 
60
  const [is_net_connected, set_is_net_connected]:any = useState(false)
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  useEffect(() => {(async () => {
 
63
  const net_info = await NetInfo.fetch()
64
  set_is_net_connected(net_info.isConnected)
65
 
@@ -67,27 +142,26 @@ const ChapterComponent = ({
67
  const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id)
68
  if (stored_chapter?.data_state === "completed") set_is_saved(true)
69
  else set_is_saved(false)
 
70
  })()}, [])
71
 
72
  useEffect(()=>{(async () => {
 
73
  const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id)
74
  if (stored_chapter?.data_state === "completed") set_is_saved(true)
75
  else set_is_saved(false)
 
76
  })()},[page,sort])
77
 
78
  const Request_Download = async (CHAPTER:any) => {
79
-
80
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
81
  if (stored_comic) {
82
  setWidgetContext({state:true,component:<RequestChapterWidget
83
  SOURCE={SOURCE}
84
  ID={ID}
85
  CHAPTER={CHAPTER}
86
- chapterQueue={chapterQueue}
87
- setChapterQueue={setChapterQueue}
88
- chapterRequested={chapterRequested}
89
- setChapterRequested={setChapterRequested}
90
- get_requested_info={() => get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID)}
91
  />})
92
  }
93
  else{
@@ -134,7 +208,8 @@ const ChapterComponent = ({
134
  if (stored_chapter?.data_state === "completed") {
135
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
136
  if (!stored_comic.history.idx || chapter.idx > stored_comic.history.idx) await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
137
- router.push(`/read/${SOURCE}/${ID}/${chapter.idx}/`)
 
138
  }else{
139
  Toast.show({
140
  type: 'error',
@@ -158,142 +233,143 @@ const ChapterComponent = ({
158
  >
159
  {chapter.title}
160
  </Button>
161
- {is_net_connected || is_saved
162
- ? <>{is_saved
163
- ? <Icon source={"content-save-check"} size={((Dimensions.width+Dimensions.height)/2)*0.0425} color={Theme[themeTypeContext].icon_color}/>
164
- : <>
165
- <>{chapterRequested[chapter.id]?.state === "queue" && !chapterQueue?.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)
166
- && <ActivityIndicator animating={true} color={Theme[themeTypeContext].icon_color} />
167
- }</>
168
-
 
 
 
169
 
170
- <>{chapterRequested[chapter.id]?.state === "unkown" && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)
171
- && <TouchableRipple
172
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
173
- style={{
174
- borderRadius:5,
175
- borderWidth:0,
176
- padding:5,
177
- }}
178
-
179
- onPress={()=>{
180
- Toast.show({
181
- type: 'error',
182
- text1: '❓Request not found in server.',
183
- text2: "You request this chapter but the server doesn't have this in queue.\nTry request again.",
184
-
185
- position: "bottom",
186
- visibilityTime: 12000,
187
- text1Style:{
188
- fontFamily:"roboto-bold",
189
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
190
- },
191
- text2Style:{
192
- fontFamily:"roboto-medium",
193
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
194
-
195
- },
196
- });
197
- Request_Download(chapter)
198
- }}
199
- >
200
- <Icon source={"alert-circle"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={"red"}/>
201
-
202
- </TouchableRipple>
203
- }</>
204
-
205
- <>{chapterRequested[chapter.id]?.state === "ready"
206
- && <>{chapterToDownload[chapter.id]?.state === "downloading"
207
- ? <CircularProgress
208
- value={downloadProgress[chapter.id].progress}
209
- maxValue={downloadProgress[chapter.id].total}
210
- radius={((Dimensions.width+Dimensions.height)/2)*0.0225}
211
- inActiveStrokeColor={Theme[themeTypeContext].border_color}
212
 
213
- showProgressValue={false}
214
- title={"📥"}
215
- titleStyle={{
216
- pointerEvents:"none",
217
- color:Theme[themeTypeContext].text_color,
218
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
219
- fontFamily:"roboto-medium",
220
- textAlign:"center",
 
 
 
 
 
 
 
 
 
 
 
221
  }}
222
- onAnimationComplete={async ()=>{
223
- if (downloadProgress[chapter.id].progress !== downloadProgress[chapter.id].total) return
 
 
 
 
 
 
 
 
 
 
 
224
 
225
- const stored_chapter_requested = (await ComicStorage.getByID(SOURCE,ID)).chapter_requested
226
- const new_chapter_requested = stored_chapter_requested.filter((item:any) => item.chapter_id !== chapter.id);
227
- await ComicStorage.updateChapterQueue(SOURCE,ID,new_chapter_requested)
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
- delete chapterRequested[chapter.id]
230
- setChapterRequested(chapterRequested)
231
 
232
- const chapter_to_download = chapterToDownload
233
- delete chapter_to_download[chapter.id]
234
- setChapterToDownload(chapter_to_download)
235
 
236
- const download_progress = downloadProgress
237
- delete download_progress[chapter.id]
238
- setDownloadProgress(download_progress)
239
 
240
- set_is_saved(true)
241
- isDownloading.current = false
242
- console.log("DONE!",downloadProgress[chapter.id])
243
- }}
244
- />
245
- : <ActivityIndicator animating={true} color={"green"} />
 
246
  }</>
247
- }</>
248
-
249
- <>{chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) && !(chapterRequested[chapter.id]?.state === "ready")
250
- && <>{chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`]
251
- ? <CircularProgress
252
- value={100 - (((chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`])*100)/chapterQueue.max_queue)}
253
- maxValue={100}
254
- radius={((Dimensions.width+Dimensions.height)/2)*0.0225}
255
- inActiveStrokeColor={Theme[themeTypeContext].border_color}
256
 
257
-
258
- showProgressValue={false}
259
- title={chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`]}
260
- titleStyle={{
261
- pointerEvents:"none",
262
- color:Theme[themeTypeContext].text_color,
263
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
264
- fontFamily:"roboto-medium",
265
- textAlign:"center",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }}
267
- onAnimationComplete={()=>{
268
- console.log("HAHA",chapterQueue)
269
- get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID)
270
  }}
271
- />
272
- : <ActivityIndicator animating={true} />
 
 
 
273
  }</>
274
- }</>
275
-
276
- <>{!chapterRequested.hasOwnProperty(chapter.id) && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)
277
- ? <TouchableRipple
278
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
279
- style={{
280
- borderRadius:5,
281
- borderWidth:0,
282
- padding:5,
283
- }}
284
-
285
- onPress={()=>{
286
- Request_Download(chapter)
287
- }}
288
- >
289
- <Icon source={"cloud-download"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={Theme[themeTypeContext].icon_color}/>
290
-
291
- </TouchableRipple>
292
- :<></>
293
- }</>
294
- </>
295
  }</>
296
- : <Icon source={"wifi-off"} size={((Dimensions.width+Dimensions.height)/2)*0.0425} color={Theme[themeTypeContext].icon_color}/>
297
  }
298
  </View>}</>
299
  }
 
12
  import { View, AnimatePresence } from 'moti';
13
  import * as Clipboard from 'expo-clipboard';
14
  import NetInfo from "@react-native-community/netinfo";
15
+ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc);
16
 
17
 
18
  import Theme from '@/constants/theme';
 
37
  chapter,
38
  signal,
39
  isDownloading,
40
+ chapter_requested,
41
+ download_progress,
42
+ chapter_to_download,
43
+ chapter_queue,
 
 
 
 
44
 
45
  }:any) => {
46
  const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT)
 
54
 
55
  const [styles, setStyles]:any = useState("")
56
  const [is_saved, set_is_saved] = useState(false)
57
+ const [isLoading, setIsLoading] = useState(true);
58
  const [is_net_connected, set_is_net_connected]:any = useState(false)
59
 
60
+
61
+ const [chapterQueue, setChapterQueue]:any = useState({})
62
+ const [chapterRequested, setChapterRequested]:any = useState({})
63
+ const [chapterToDownload, setChapterToDownload]:any = useState({})
64
+ const [downloadProgress, setDownloadProgress]:any = useState({[chapter.id]:{progress:0,total:100}})
65
+
66
+
67
+ const chapter_status_interval = useRef<any>(null)
68
+ const is_running = useRef<boolean>(false)
69
+
70
+ useEffect(()=>{
71
+ // console.log("CTD", chapterToDownload)
72
+ },[chapterToDownload])
73
+
74
+ useEffect(()=>{
75
+ // console.log("CR", chapterRequested)
76
+ },[chapterRequested])
77
+
78
+ useFocusEffect(useCallback(() => {
79
+ clearInterval(chapter_status_interval.current)
80
+ chapter_status_interval.current = setInterval(() => {
81
+ if (is_running.current) return
82
+ is_running.current = true
83
+
84
+ // current -> Checking for chapter requested
85
+ if (chapter_requested.current.hasOwnProperty(chapter.id) && !chapterRequested.hasOwnProperty(chapter.id)) {
86
+ setChapterRequested({[chapter.id]:chapter_requested.current[chapter.id]})
87
+ }else if (
88
+ chapter_requested.current.hasOwnProperty(chapter.id)
89
+ && chapter_requested.current[chapter.id]?.state !== chapterRequested[chapter.id]?.state
90
+ ) {
91
+ setChapterRequested({[chapter.id]:chapter_requested.current[chapter.id]})
92
+ }else if (!chapter_requested.current.hasOwnProperty(chapter.id) && chapterRequested.hasOwnProperty(chapter.id)){
93
+ setChapterRequested({})
94
+ }
95
+
96
+ // current -> Checking for chapter to download
97
+ if (chapter_to_download.current.hasOwnProperty(chapter.id) && !chapterToDownload.hasOwnProperty(chapter.id)) {
98
+ setChapterToDownload({[chapter.id]:chapter_to_download.current[chapter.id]})
99
+ }else if (
100
+ chapter_to_download.current.hasOwnProperty(chapter.id)
101
+ && chapter_to_download.current[chapter.id]?.state !== chapterToDownload[chapter.id]?.state
102
+ ) {
103
+ setChapterToDownload({[chapter.id]:chapter_to_download.current[chapter.id]})
104
+ }else if (!chapter_to_download.current.hasOwnProperty(chapter.id) && chapterToDownload.hasOwnProperty(chapter.id)){
105
+ setChapterToDownload({})
106
+ }
107
+
108
+ // current -> Check download progress
109
+ if (download_progress.current.hasOwnProperty(chapter.id) && !downloadProgress.hasOwnProperty(chapter.id)) {
110
+ setDownloadProgress({[chapter.id]:download_progress.current[chapter.id]})
111
+ }else if (
112
+ download_progress.current.hasOwnProperty(chapter.id)
113
+ && (
114
+ download_progress.current[chapter.id]?.progress !== downloadProgress[chapter.id]?.progress
115
+ || download_progress.current[chapter.id]?.total !== downloadProgress[chapter.id]?.total
116
+ )
117
+ ) {
118
+ setDownloadProgress({[chapter.id]:download_progress.current[chapter.id]})
119
+ }else if (!download_progress.current.hasOwnProperty(chapter.id) && downloadProgress.hasOwnProperty(chapter.id)){
120
+ setDownloadProgress({})
121
+ }
122
+
123
+ // current -> Check chapter Queue
124
+ if (chapter_queue.current?.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)) {
125
+ setChapterQueue(chapter_queue.current)
126
+ }else if (chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)){
127
+ setChapterQueue({})
128
+ }
129
+
130
+ is_running.current = false
131
+ },3000)
132
+ return () => clearInterval(chapter_status_interval.current)
133
+ },[chapterQueue,chapterRequested,chapterToDownload, downloadProgress]))
134
+
135
+
136
  useEffect(() => {(async () => {
137
+ setIsLoading(true)
138
  const net_info = await NetInfo.fetch()
139
  set_is_net_connected(net_info.isConnected)
140
 
 
142
  const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id)
143
  if (stored_chapter?.data_state === "completed") set_is_saved(true)
144
  else set_is_saved(false)
145
+ setIsLoading(false)
146
  })()}, [])
147
 
148
  useEffect(()=>{(async () => {
149
+ setIsLoading(true)
150
  const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id)
151
  if (stored_chapter?.data_state === "completed") set_is_saved(true)
152
  else set_is_saved(false)
153
+ setIsLoading(false)
154
  })()},[page,sort])
155
 
156
  const Request_Download = async (CHAPTER:any) => {
 
157
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
158
  if (stored_comic) {
159
  setWidgetContext({state:true,component:<RequestChapterWidget
160
  SOURCE={SOURCE}
161
  ID={ID}
162
  CHAPTER={CHAPTER}
163
+ chapter_requested={chapter_requested}
164
+ get_requested_info={() => get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID)}
 
 
 
165
  />})
166
  }
167
  else{
 
208
  if (stored_chapter?.data_state === "completed") {
209
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
210
  if (!stored_comic.history.idx || chapter.idx > stored_comic.history.idx) await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
211
+
212
+ router.navigate(`/read/${SOURCE}/${ID}/${chapter.idx}/`)
213
  }else{
214
  Toast.show({
215
  type: 'error',
 
233
  >
234
  {chapter.title}
235
  </Button>
236
+ {isLoading
237
+ ? (<ActivityIndicator animating={true} color={Theme[themeTypeContext].icon_color} />)
238
+
239
+ : <>{is_net_connected || is_saved
240
+ ? <>{is_saved
241
+ ? <Icon source={"content-save-check"} size={((Dimensions.width+Dimensions.height)/2)*0.0425} color={Theme[themeTypeContext].icon_color}/>
242
+ : <>
243
+ <>{chapterRequested[chapter.id]?.state === "queue" && !chapterQueue?.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)
244
+ && <ActivityIndicator animating={true} color={"#7df9ff"} />
245
+ }</>
246
+
247
 
248
+ <>{chapterRequested[chapter.id]?.state === "unkown" && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)
249
+ && <TouchableRipple
250
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
251
+ style={{
252
+ borderRadius:5,
253
+ borderWidth:0,
254
+ padding:5,
255
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
+ onPress={()=>{
258
+ Toast.show({
259
+ type: 'error',
260
+ text1: '❓Request not found in server.',
261
+ text2: "You request this chapter but the server doesn't have this in queue.\nTry request again.",
262
+
263
+ position: "bottom",
264
+ visibilityTime: 12000,
265
+ text1Style:{
266
+ fontFamily:"roboto-bold",
267
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
268
+ },
269
+ text2Style:{
270
+ fontFamily:"roboto-medium",
271
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
272
+
273
+ },
274
+ });
275
+ Request_Download(chapter)
276
  }}
277
+ >
278
+ <Icon source={"alert-circle"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={"red"}/>
279
+
280
+ </TouchableRipple>
281
+ }</>
282
+
283
+ <>{chapterRequested[chapter.id]?.state === "ready"
284
+ && <>{chapterToDownload[chapter.id]?.state === "downloading"
285
+ ? <CircularProgress
286
+ value={downloadProgress[chapter.id].progress}
287
+ maxValue={downloadProgress[chapter.id].total}
288
+ radius={((Dimensions.width+Dimensions.height)/2)*0.0225}
289
+ inActiveStrokeColor={Theme[themeTypeContext].border_color}
290
 
291
+ showProgressValue={false}
292
+ title={"📥"}
293
+ titleStyle={{
294
+ pointerEvents:"none",
295
+ color:Theme[themeTypeContext].text_color,
296
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
297
+ fontFamily:"roboto-medium",
298
+ textAlign:"center",
299
+ }}
300
+ onAnimationComplete={async ()=>{
301
+ if (downloadProgress[chapter.id].progress !== downloadProgress[chapter.id].total) return
302
+
303
+ const stored_chapter_requested = (await ComicStorage.getByID(SOURCE,ID)).chapter_requested
304
+ const new_chapter_requested = stored_chapter_requested.filter((item:any) => item.chapter_id !== chapter.id);
305
+ await ComicStorage.updateChapterQueue(SOURCE,ID,new_chapter_requested)
306
 
307
+ delete chapter_requested.current[chapter.id]
308
+ setChapterRequested({})
309
 
310
+ delete chapter_to_download.current[chapter.id]
311
+ setChapterToDownload({})
 
312
 
313
+ delete download_progress[chapter.id]
314
+ setDownloadProgress({})
 
315
 
316
+ set_is_saved(true)
317
+ isDownloading.current = false
318
+ console.log("DONE DOWNLOADING!")
319
+ }}
320
+ />
321
+ : <ActivityIndicator animating={true} color={"green"} />
322
+ }</>
323
  }</>
324
+
325
+ <>{chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) && !(chapterRequested[chapter.id]?.state === "ready")
326
+ && <>{chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`]
327
+ ? <CircularProgress
328
+ value={100 - (((chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`])*100)/chapterQueue.max_queue)}
329
+ maxValue={100}
330
+ radius={((Dimensions.width+Dimensions.height)/2)*0.0225}
331
+ inActiveStrokeColor={Theme[themeTypeContext].border_color}
332
+
333
 
334
+ showProgressValue={false}
335
+ title={chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`]}
336
+ titleStyle={{
337
+ pointerEvents:"none",
338
+ color:Theme[themeTypeContext].text_color,
339
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025,
340
+ fontFamily:"roboto-medium",
341
+ textAlign:"center",
342
+ }}
343
+ onAnimationComplete={()=>{
344
+ get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, chapter_to_download, signal, SOURCE, ID)
345
+ }}
346
+ />
347
+ : <ActivityIndicator animating={true} />
348
+ }</>
349
+ }</>
350
+
351
+ <>{!chapterRequested.hasOwnProperty(chapter.id) && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)
352
+ ? <TouchableRipple
353
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
354
+ style={{
355
+ borderRadius:5,
356
+ borderWidth:0,
357
+ padding:5,
358
  }}
359
+
360
+ onPress={()=>{
361
+ Request_Download(chapter)
362
  }}
363
+ >
364
+ <Icon source={"cloud-download"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={Theme[themeTypeContext].icon_color}/>
365
+
366
+ </TouchableRipple>
367
+ :<></>
368
  }</>
369
+ </>
370
+ }</>
371
+ : <Icon source={"wifi-off"} size={((Dimensions.width+Dimensions.height)/2)*0.0425} color={Theme[themeTypeContext].icon_color}/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  }</>
 
373
  }
374
  </View>}</>
375
  }
frontend/app/view/componenets/widgets/bookmark.tsx CHANGED
@@ -1,5 +1,5 @@
1
 
2
- import React, { useEffect, useState, useCallback, useContext, useRef, Fragment } from 'react';
3
  import { Platform, useWindowDimensions, ScrollView } from 'react-native';
4
 
5
  import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper';
@@ -17,9 +17,10 @@ import Storage from '@/constants/module/storages/storage';
17
  import ComicStorage from '@/constants/module/storages/comic_storage';
18
  import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
19
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
20
-
21
 
22
  interface BookmarkWidgetProps {
 
23
  onRefresh: any;
24
  SOURCE: string | string[];
25
  ID: string | string[];
@@ -27,10 +28,11 @@ interface BookmarkWidgetProps {
27
  }
28
 
29
  const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
 
30
  onRefresh,
31
  SOURCE,
32
  ID,
33
- CONTENT
34
  }) => {
35
  const Dimensions = useWindowDimensions();
36
 
@@ -58,206 +60,207 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
58
  const controller = new AbortController();
59
  const signal = controller.signal;
60
 
61
- const RenderTag = ({item}:any) =>{
62
- const [editTag, setEditTag]:any = useState(item.value)
63
- return (<>
64
- {item.value.includes(searchTag) &&
65
- (
66
- <View
67
- style={{
68
- display:"flex",
69
- flexDirection:"row",
70
- alignItems:"center",
71
- justifyContent:"space-between",
72
- gap:8,
73
- zIndex:10,
74
- }}
75
- >
76
- <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value &&
77
- (<View
78
- style={{
79
- width:"100%",
80
- display:"flex",
81
- flexDirection:"row",
82
- justifyContent:"space-between",
83
- alignItems:"center",
84
- height:"auto",
85
- gap:18,
86
- }}
87
- >
88
- <Text
89
- style={{
90
- color:"white",
91
- fontFamily:"roboto-medium",
92
- fontSize:(Dimensions.width+Dimensions.height)/2*0.025
93
- }}
94
- >{item.label}</Text>
95
- <View
96
  style={{
97
- width:"auto",
 
 
 
 
98
  height:"auto",
99
-
100
  }}
101
  >
102
-
103
- <TouchableRipple
104
-
105
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
106
  style={{
107
- borderRadius:5,
108
- borderWidth:0,
109
- backgroundColor: "transparent",
110
- padding:5,
111
-
112
- }}
113
-
114
- onPress={(event)=>{
115
- if (manageBookmark.edit){
116
- setManageBookmark({...manageBookmark,edit:""})
117
- setEditTag("")
118
- }
119
-
120
-
121
- const x = event.nativeEvent.pageX
122
- const y = event.nativeEvent.pageY
123
-
124
- setShowMenuOption({
125
- ...showMenuOption,
126
- state: showMenuOption.id === item.value ? false : true,
127
- positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0],
128
- id:showMenuOption.id === item.value ? "" : item.value,
129
- })
130
-
131
-
132
-
133
  }}
134
- >
135
-
136
- <Icon source={"dots-vertical"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={Theme[themeTypeContext].icon_color}/>
137
- </TouchableRipple>
138
- </View>
139
- </View>)
140
- }</>
141
- <>{manageBookmark.edit &&
142
- (<View
143
- style={{
144
- display:"flex",
145
- flexDirection:"row",
146
- justifyContent:"space-between",
147
- alignItems:"center",
148
- width:"100%",
149
- height:"auto",
150
- gap:12,
151
- padding:12,
152
- }}
153
- >
154
- <View
155
- style={{flex:1}}
156
- >
157
- <TextInput mode="outlined" label="Edit" textColor={Theme[themeTypeContext].text_color} maxLength={72}
158
- right={<TextInput.Affix text={`| Max: 72`} />}
159
- style={{
160
- width:"100%",
161
- height:"100%",
162
- backgroundColor:Theme[themeTypeContext].background_color,
163
- borderColor:Theme[themeTypeContext].border_color,
164
-
165
- }}
166
- outlineColor={Theme[themeTypeContext].text_input_border_color}
167
- value={editTag}
168
- onChange={(event)=>{
169
- setEditTag(event.nativeEvent.text)
170
- }}
171
- />
172
- </View>
173
  <View
174
  style={{
175
- display:"flex",
176
- flexDirection:"row",
177
- gap:8,
178
  }}
179
  >
 
180
  <TouchableRipple
 
181
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
182
  style={{
183
  borderRadius:5,
184
  borderWidth:0,
185
  backgroundColor: "transparent",
186
  padding:5,
 
187
  }}
188
 
189
- onPress={()=>{
190
- setManageBookmark({...manageBookmark,edit:""})
191
- setEditTag("")
192
- setShowMenuOption({...showMenuOption,state:false,id:""})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }}
194
  >
195
 
196
- <Icon source={"close"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"red"}/>
197
  </TouchableRipple>
198
- <TouchableRipple
199
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  style={{
201
- borderRadius:5,
202
- borderWidth:0,
203
- backgroundColor: "transparent",
204
- padding:5,
205
  }}
 
 
 
 
 
 
 
 
 
206
 
207
- onPress={async ()=>{
208
- const stored_bookmark = await Storage.get("bookmark");
 
 
 
 
209
 
210
- const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit);
211
-
212
- if (index !== -1){
213
- stored_bookmark[index] = editTag;
214
- await Storage.store("bookmark", stored_bookmark)
 
 
 
 
 
215
 
216
- const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit)
217
- for (const item of stored_comics){
218
- await ComicStorage.replaceTag(item.source, item.id, editTag)
219
- }
220
- if (manageBookmark.edit === defaultTag) {
221
- onRefresh();
222
- setWidgetContext({state:false,component:<></>});
 
223
 
224
- }else{
225
-
226
- const index = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit);
227
- if (index !== -1){
228
- BOOKMARK_DATA[index].label = editTag
229
- BOOKMARK_DATA[index].value = editTag
 
 
 
 
 
 
 
 
 
 
 
 
230
  }
231
- SET_BOOKMARK_DATA(BOOKMARK_DATA)
232
- setManageBookmark({...manageBookmark,edit:""})
233
- setEditTag("")
234
  }
235
-
236
- }
237
- setShowMenuOption({...showMenuOption,state:false,id:""})
238
- }}
239
- >
 
 
 
240
 
241
- <Icon source={"check"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"green"}/>
242
- </TouchableRipple>
243
- </View>
244
-
245
-
246
 
247
- </View>)
248
 
249
- }</>
250
 
 
 
 
 
251
 
252
-
253
-
254
- </View>
255
-
256
- )
257
- }
258
- </>)
259
- }
260
-
261
 
262
 
263
  const load_bookmark = async ()=>{
@@ -283,7 +286,6 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
283
  }
284
 
285
  useEffect(()=>{
286
- console.log(BOOKMARK_DATA)
287
  SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA])
288
  },[BOOKMARK_DATA])
289
 
@@ -298,8 +300,8 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
298
  style={{
299
  zIndex:10,
300
  backgroundColor:Theme[themeTypeContext].background_color,
301
- width:Dimensions.width*0.35,
302
- minWidth:500,
303
 
304
  borderColor:Theme[themeTypeContext].border_color,
305
  borderWidth:2,
@@ -351,7 +353,7 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
351
  theme_type={themeTypeContext}
352
  Dimensions={Dimensions}
353
 
354
- label='Add to bookmark'
355
  data={BOOKMARK_DATA}
356
  value={bookmark}
357
  onChange={(async (item:any) => {
@@ -420,10 +422,9 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
420
  const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
421
  if (stored_comic) await ComicStorage.replaceTag(SOURCE, CONTENT.id, bookmark)
422
  else {
423
- const cover_result:any = await store_comic_cover(setShowCloudflareTurnstileContext,signal,SOURCE,ID,CONTENT)
424
 
425
  await ComicStorage.store(SOURCE,CONTENT.id, bookmark, {
426
- cover:cover_result,
427
  title:CONTENT.title,
428
  author:CONTENT.author,
429
  category:CONTENT.category,
@@ -457,7 +458,7 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
457
  style={{flex:1}}
458
  >
459
  <TextInput mode="outlined" label="Search" textColor={Theme[themeTypeContext].text_color}
460
- placeholder={""}
461
  style={{
462
 
463
  backgroundColor:Theme[themeTypeContext].background_color,
@@ -491,10 +492,14 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
491
 
492
  }}
493
  >
494
- <>{BOOKMARK_DATA.map((item:any) => <Fragment key={item.value}>
495
- <RenderTag item={item}/>
496
- <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].border_color}}/>
497
- </Fragment>)}</>
 
 
 
 
498
  </ScrollView>
499
  </View>
500
  </>
@@ -505,7 +510,7 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
505
  color:Theme[themeTypeContext].text_color,
506
  fontSize:(Dimensions.width+Dimensions.height)/2*0.045,
507
  fontFamily:"roboto-bold",
508
- }}>No bookmark found</Text>
509
  </>
510
 
511
  }</>
@@ -530,7 +535,7 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
530
  setCreateTag({state:true,title:""})
531
  setShowMenuOption({...showMenuOption,state:false,id:""})
532
  })}
533
- >+ Create Bookmark</Button>
534
  <Button mode='outlined'
535
  labelStyle={{
536
  color:Theme[themeTypeContext].text_color,
@@ -564,7 +569,7 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
564
  gap:12,
565
  }}
566
  >
567
- <TextInput mode="outlined" label="Create Bookmark" textColor={Theme[themeTypeContext].text_color} maxLength={72}
568
  placeholder="Bookmark Tag"
569
 
570
  right={<TextInput.Affix text={`| Max: 72`} />}
@@ -738,6 +743,8 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
738
  }}
739
  style={{backgroundColor:"red",borderRadius:5}}
740
  onPress={(async ()=>{
 
 
741
  setRemoveTag({...removeTag,removing:false})
742
  if (Platform.OS !== "web"){
743
  const comic_dir = FileSystem.documentDirectory + "ComicMTL/" + `${SOURCE}/` + `${ID}/`
@@ -745,10 +752,10 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
745
  }
746
 
747
  await ChapterStorage.drop(`${SOURCE}-${CONTENT.id}`)
 
748
  await ComicStorage.removeByID(SOURCE,CONTENT.id)
749
-
750
  onRefresh()
751
- setWidgetContext({state:false,component:<></>})
752
  })}
753
  >Yes</Button>
754
  </>
@@ -809,12 +816,12 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
809
 
810
  }}
811
  >
812
- <Icon source={"pencil"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"blue"}/>
813
  <View>
814
  <Text selectable={false}
815
  style={{
816
  textAlign:"center",
817
- color:"blue",
818
  fontFamily:"roboto-medium",
819
  fontSize:(Dimensions.width+Dimensions.height)/2*0.02
820
  }}
@@ -881,13 +888,14 @@ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
881
  display:"flex",
882
  justifyContent:"center",
883
  alignItems:"center",
 
884
  }}
885
  >
886
  <View
887
  style={{
888
  backgroundColor:Theme[themeTypeContext].background_color,
889
- width:Dimensions.width*0.35,
890
- minWidth:500,
891
  height:"auto",
892
 
893
  borderColor:Theme[themeTypeContext].border_color,
 
1
 
2
+ import React, { useEffect, useState, useCallback, useContext, useRef, Fragment, memo } from 'react';
3
  import { Platform, useWindowDimensions, ScrollView } from 'react-native';
4
 
5
  import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper';
 
17
  import ComicStorage from '@/constants/module/storages/comic_storage';
18
  import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
19
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
20
+ import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage';
21
 
22
  interface BookmarkWidgetProps {
23
+ setIsLoading: any;
24
  onRefresh: any;
25
  SOURCE: string | string[];
26
  ID: string | string[];
 
28
  }
29
 
30
  const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
31
+ setIsLoading,
32
  onRefresh,
33
  SOURCE,
34
  ID,
35
+ CONTENT,
36
  }) => {
37
  const Dimensions = useWindowDimensions();
38
 
 
60
  const controller = new AbortController();
61
  const signal = controller.signal;
62
 
63
+ const RenderTag = useCallback(({item}:any) =>{
64
+ const [editTag, setEditTag]:any = useState(item.value)
65
+ useEffect(()=>{
66
+ },[manageBookmark])
67
+
68
+ return (<>
69
+ {item.value.includes(searchTag) &&
70
+ (
71
+ <View
72
+ style={{
73
+ display:"flex",
74
+ flexDirection:"row",
75
+ alignItems:"center",
76
+ justifyContent:"space-between",
77
+ gap:8,
78
+ zIndex:10,
79
+ }}
80
+ >
81
+ <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value &&
82
+ (<View
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  style={{
84
+ width:"100%",
85
+ display:"flex",
86
+ flexDirection:"row",
87
+ justifyContent:"space-between",
88
+ alignItems:"center",
89
  height:"auto",
90
+ gap:18,
91
  }}
92
  >
93
+ <Text
 
 
 
94
  style={{
95
+ color:"white",
96
+ fontFamily:"roboto-medium",
97
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }}
99
+ >{item.label}</Text>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  <View
101
  style={{
102
+ width:"auto",
103
+ height:"auto",
104
+
105
  }}
106
  >
107
+
108
  <TouchableRipple
109
+
110
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
111
  style={{
112
  borderRadius:5,
113
  borderWidth:0,
114
  backgroundColor: "transparent",
115
  padding:5,
116
+
117
  }}
118
 
119
+ onPress={(event)=>{
120
+ if (manageBookmark.edit){
121
+ setManageBookmark({...manageBookmark,edit:""})
122
+ setEditTag("")
123
+ }
124
+
125
+ const x = event.nativeEvent.pageX
126
+ const y = event.nativeEvent.pageY
127
+
128
+ setShowMenuOption({
129
+ ...showMenuOption,
130
+ state: showMenuOption.id === item.value ? false : true,
131
+ positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0],
132
+ id:showMenuOption.id === item.value ? "" : item.value,
133
+ })
134
+
135
+
136
+
137
  }}
138
  >
139
 
140
+ <Icon source={"dots-vertical"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={Theme[themeTypeContext].icon_color}/>
141
  </TouchableRipple>
142
+ </View>
143
+ </View>)
144
+ }</>
145
+ <>{manageBookmark.edit === item.value &&
146
+ (<View
147
+ style={{
148
+ display:"flex",
149
+ flexDirection:"row",
150
+ justifyContent:"space-between",
151
+ alignItems:"center",
152
+ width:"100%",
153
+ height:"auto",
154
+ gap:12,
155
+ padding:12,
156
+ }}
157
+ >
158
+ <View
159
+ style={{flex:1}}
160
+ >
161
+ <TextInput mode="outlined" label="Edit" textColor={Theme[themeTypeContext].text_color} maxLength={72}
162
+ autoFocus={true}
163
+ right={<TextInput.Affix text={`| Max: 72`} />}
164
+ style={{
165
+ backgroundColor:Theme[themeTypeContext].background_color,
166
+ borderColor:Theme[themeTypeContext].border_color,
167
+
168
+ }}
169
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
170
+ value={editTag}
171
+ onChangeText={(text)=>{
172
+ setEditTag(text)
173
+ }}
174
+ />
175
+ </View>
176
+ <View
177
  style={{
178
+ display:"flex",
179
+ flexDirection:"row",
180
+ gap:8,
 
181
  }}
182
+ >
183
+ <TouchableRipple
184
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
185
+ style={{
186
+ borderRadius:5,
187
+ borderWidth:0,
188
+ backgroundColor: "transparent",
189
+ padding:5,
190
+ }}
191
 
192
+ onPress={()=>{
193
+ setManageBookmark({...manageBookmark,edit:""})
194
+ setEditTag("")
195
+ setShowMenuOption({...showMenuOption,state:false,id:""})
196
+ }}
197
+ >
198
 
199
+ <Icon source={"close"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"red"}/>
200
+ </TouchableRipple>
201
+ <TouchableRipple
202
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
203
+ style={{
204
+ borderRadius:5,
205
+ borderWidth:0,
206
+ backgroundColor: "transparent",
207
+ padding:5,
208
+ }}
209
 
210
+ onPress={async ()=>{
211
+ const stored_bookmark = await Storage.get("bookmark");
212
+
213
+ const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit);
214
+
215
+ if (index !== -1){
216
+ stored_bookmark[index] = editTag;
217
+ await Storage.store("bookmark", stored_bookmark)
218
 
219
+ const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit)
220
+ for (const item of stored_comics){
221
+ await ComicStorage.replaceTag(item.source, item.id, editTag)
222
+ }
223
+ if ((manageBookmark.edit === defaultTag) && (editTag !== defaultTag)) {
224
+ onRefresh();
225
+ setWidgetContext({state:false,component:<></>});
226
+
227
+ }else{
228
+
229
+ const index = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit);
230
+ if (index !== -1){
231
+ BOOKMARK_DATA[index].label = editTag
232
+ BOOKMARK_DATA[index].value = editTag
233
+ }
234
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
235
+ setManageBookmark({...manageBookmark,edit:""})
236
+ setEditTag("")
237
  }
238
+
 
 
239
  }
240
+ setShowMenuOption({...showMenuOption,state:false,id:""})
241
+ }}
242
+ >
243
+
244
+ <Icon source={"check"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"green"}/>
245
+ </TouchableRipple>
246
+ </View>
247
+
248
 
 
 
 
 
 
249
 
250
+ </View>)
251
 
252
+ }</>
253
 
254
+
255
+
256
+
257
+ </View>
258
 
259
+ )
260
+ }
261
+ </>)
262
+ }
263
+ ,[Theme,themeTypeContext,manageBookmark,searchTag,removeTag,createTag,defaultTag,bookmark,showMenuOption,MIGRATE_BOOKMARK_DATA,BOOKMARK_DATA])
 
 
 
 
264
 
265
 
266
  const load_bookmark = async ()=>{
 
286
  }
287
 
288
  useEffect(()=>{
 
289
  SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA])
290
  },[BOOKMARK_DATA])
291
 
 
300
  style={{
301
  zIndex:10,
302
  backgroundColor:Theme[themeTypeContext].background_color,
303
+ maxWidth:500,
304
+ width:"100%",
305
 
306
  borderColor:Theme[themeTypeContext].border_color,
307
  borderWidth:2,
 
353
  theme_type={themeTypeContext}
354
  Dimensions={Dimensions}
355
 
356
+ label='Add to tag'
357
  data={BOOKMARK_DATA}
358
  value={bookmark}
359
  onChange={(async (item:any) => {
 
422
  const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
423
  if (stored_comic) await ComicStorage.replaceTag(SOURCE, CONTENT.id, bookmark)
424
  else {
425
+ await store_comic_cover(setShowCloudflareTurnstileContext,signal,SOURCE,ID,CONTENT)
426
 
427
  await ComicStorage.store(SOURCE,CONTENT.id, bookmark, {
 
428
  title:CONTENT.title,
429
  author:CONTENT.author,
430
  category:CONTENT.category,
 
458
  style={{flex:1}}
459
  >
460
  <TextInput mode="outlined" label="Search" textColor={Theme[themeTypeContext].text_color}
461
+
462
  style={{
463
 
464
  backgroundColor:Theme[themeTypeContext].background_color,
 
492
 
493
  }}
494
  >
495
+ <>{BOOKMARK_DATA.map((item:any) =>
496
+ (
497
+ <View key={item.value}>
498
+ <RenderTag item={item}/>
499
+ <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].border_color}}/>
500
+ </View>
501
+ )
502
+ )}</>
503
  </ScrollView>
504
  </View>
505
  </>
 
510
  color:Theme[themeTypeContext].text_color,
511
  fontSize:(Dimensions.width+Dimensions.height)/2*0.045,
512
  fontFamily:"roboto-bold",
513
+ }}>No tag found</Text>
514
  </>
515
 
516
  }</>
 
535
  setCreateTag({state:true,title:""})
536
  setShowMenuOption({...showMenuOption,state:false,id:""})
537
  })}
538
+ >+ Create</Button>
539
  <Button mode='outlined'
540
  labelStyle={{
541
  color:Theme[themeTypeContext].text_color,
 
569
  gap:12,
570
  }}
571
  >
572
+ <TextInput mode="outlined" label="Create Tag" textColor={Theme[themeTypeContext].text_color} maxLength={72}
573
  placeholder="Bookmark Tag"
574
 
575
  right={<TextInput.Affix text={`| Max: 72`} />}
 
743
  }}
744
  style={{backgroundColor:"red",borderRadius:5}}
745
  onPress={(async ()=>{
746
+ setWidgetContext({state:false,component:<></>})
747
+ setIsLoading(true)
748
  setRemoveTag({...removeTag,removing:false})
749
  if (Platform.OS !== "web"){
750
  const comic_dir = FileSystem.documentDirectory + "ComicMTL/" + `${SOURCE}/` + `${ID}/`
 
752
  }
753
 
754
  await ChapterStorage.drop(`${SOURCE}-${CONTENT.id}`)
755
+ await ChapterDataStorage.removeByComicID(CONTENT.id)
756
  await ComicStorage.removeByID(SOURCE,CONTENT.id)
757
+
758
  onRefresh()
 
759
  })}
760
  >Yes</Button>
761
  </>
 
816
 
817
  }}
818
  >
819
+ <Icon source={"pencil"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"cyan"}/>
820
  <View>
821
  <Text selectable={false}
822
  style={{
823
  textAlign:"center",
824
+ color:"cyan",
825
  fontFamily:"roboto-medium",
826
  fontSize:(Dimensions.width+Dimensions.height)/2*0.02
827
  }}
 
888
  display:"flex",
889
  justifyContent:"center",
890
  alignItems:"center",
891
+ padding:15,
892
  }}
893
  >
894
  <View
895
  style={{
896
  backgroundColor:Theme[themeTypeContext].background_color,
897
+ maxWidth:500,
898
+ width:"100%",
899
  height:"auto",
900
 
901
  borderColor:Theme[themeTypeContext].border_color,
frontend/app/view/componenets/widgets/request_chapter.tsx CHANGED
@@ -23,21 +23,15 @@ interface RequestChapterWidgetProps {
23
  SOURCE: string | string[];
24
  ID: string | string[];
25
  CHAPTER: any;
26
- chapterQueue: any;
27
- setChapterQueue: any;
28
- chapterRequested: any;
29
- setChapterRequested: any;
30
  get_requested_info: any;
31
- }
32
 
33
  const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
34
  SOURCE,
35
  ID,
36
  CHAPTER,
37
- chapterQueue,
38
- setChapterQueue,
39
- chapterRequested,
40
- setChapterRequested,
41
  get_requested_info
42
  }) => {
43
  const Dimensions = useWindowDimensions();
@@ -102,7 +96,7 @@ const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
102
  theme_type={themeTypeContext}
103
  Dimensions={Dimensions}
104
 
105
- label='Colorize'
106
  data={[
107
  {
108
  label: "Enable",
@@ -122,7 +116,7 @@ const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
122
  theme_type={themeTypeContext}
123
  Dimensions={Dimensions}
124
 
125
- label='Translation'
126
  data={[
127
  {
128
  label: "Enable",
@@ -219,9 +213,7 @@ const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
219
  style={{backgroundColor:"green",borderRadius:5}}
220
  onPress={(async ()=>{
221
  setIsRequesting(true)
222
- const new_queue:any = {}
223
- new_queue[CHAPTER.id] = "queue"
224
- setChapterRequested({...chapterRequested,...new_queue})
225
 
226
  const API_BASE = await Storage.get("IN_USE_API_BASE")
227
  const stored_socket_info = await Storage.get("SOCKET_INFO")
 
23
  SOURCE: string | string[];
24
  ID: string | string[];
25
  CHAPTER: any;
26
+ chapter_requested: any;
 
 
 
27
  get_requested_info: any;
28
+ }
29
 
30
  const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
31
  SOURCE,
32
  ID,
33
  CHAPTER,
34
+ chapter_requested,
 
 
 
35
  get_requested_info
36
  }) => {
37
  const Dimensions = useWindowDimensions();
 
96
  theme_type={themeTypeContext}
97
  Dimensions={Dimensions}
98
 
99
+ label='Colorization'
100
  data={[
101
  {
102
  label: "Enable",
 
116
  theme_type={themeTypeContext}
117
  Dimensions={Dimensions}
118
 
119
+ label='Translation (Beta)'
120
  data={[
121
  {
122
  label: "Enable",
 
213
  style={{backgroundColor:"green",borderRadius:5}}
214
  onPress={(async ()=>{
215
  setIsRequesting(true)
216
+ chapter_requested.current[CHAPTER.id] = {state: "queue"}
 
 
217
 
218
  const API_BASE = await Storage.get("IN_USE_API_BASE")
219
  const stored_socket_info = await Storage.get("SOCKET_INFO")