diff --git a/backend/__pycache__/urls.cpython-312.pyc b/backend/__pycache__/urls.cpython-312.pyc index 5569dc4c8b92c225fc6a3ab278f5e642976fb761..797707dcc6c79c07a3024d9dc1cf8df358103b2d 100644 Binary files a/backend/__pycache__/urls.cpython-312.pyc and b/backend/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/api/__pycache__/queue.cpython-312.pyc b/backend/api/__pycache__/queue.cpython-312.pyc index b3591b29bfdf06dc40abb7a22688dd16613eda5c..01c26e25834ace5d74378391ac987be6af297611 100644 Binary files a/backend/api/__pycache__/queue.cpython-312.pyc and b/backend/api/__pycache__/queue.cpython-312.pyc differ diff --git a/backend/api/__pycache__/stream_file.cpython-312.pyc b/backend/api/__pycache__/stream_file.cpython-312.pyc index 4d410923cd04a40df0e0d8c8a00e218a3147c68d..8abc50d9ad0820233b3ccbea60f6288dc7238bb8 100644 Binary files a/backend/api/__pycache__/stream_file.cpython-312.pyc and b/backend/api/__pycache__/stream_file.cpython-312.pyc differ diff --git a/backend/api/__pycache__/web_scrap.cpython-312.pyc b/backend/api/__pycache__/web_scrap.cpython-312.pyc index 8676c63179062d6f00e27730618cd59b6059a1c0..1bbdd9858bc84cdc8497afb2323c829aee794c0f 100644 Binary files a/backend/api/__pycache__/web_scrap.cpython-312.pyc and b/backend/api/__pycache__/web_scrap.cpython-312.pyc differ diff --git a/backend/api/__pycache__/web_scrape.cpython-312.pyc b/backend/api/__pycache__/web_scrape.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f864fb6c42f9d1f586c00a6e2b30831292eabbfc Binary files /dev/null and b/backend/api/__pycache__/web_scrape.cpython-312.pyc differ diff --git a/backend/api/queue.py b/backend/api/queue.py index 0a294415f9c61af977c283250bb32f20bc588a92..18314056b968e8d811223a37136768ad6f5fc411 100644 --- a/backend/api/queue.py +++ b/backend/api/queue.py @@ -9,7 +9,8 @@ from asgiref.sync import sync_to_async from backend.module import web_scrap from backend.module.utils import manage_image -from backend.models.model_cache import SocketRequestChapterQueueCache, ComicStorageCache +from backend.models.model_cache import SocketRequestChapterQueueCache +from backend.models.model_1 import ComicStorageCache from core.settings import BASE_DIR from backend.module.utils import cloudflare_turnstile diff --git a/backend/api/stream_file.py b/backend/api/stream_file.py index f9bca2583a5f277d24c93334b079df62e49a6bde..2247ace583258b2dfca24d16504720e8ea4b3b27 100644 --- a/backend/api/stream_file.py +++ b/backend/api/stream_file.py @@ -3,7 +3,8 @@ from core.settings import BASE_DIR from django_ratelimit.decorators import ratelimit from django.views.decorators.csrf import csrf_exempt from backend.module.utils import cloudflare_turnstile -from backend.models.model_cache import SocketRequestChapterQueueCache, ComicStorageCache +from backend.models.model_cache import SocketRequestChapterQueueCache +from backend.models.model_1 import ComicStorageCache import os, json, sys diff --git a/backend/api/web_scrap.py b/backend/api/web_scrap.py deleted file mode 100644 index 192759a49b7d6a6bde3c5f50bf0f41e5a7bb321f..0000000000000000000000000000000000000000 --- a/backend/api/web_scrap.py +++ /dev/null @@ -1,82 +0,0 @@ - -import json, environ, requests, os, subprocess -import asyncio, uuid - -from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest -from django_ratelimit.decorators import ratelimit -from django.views.decorators.csrf import csrf_exempt -from asgiref.sync import sync_to_async - -from backend.module import web_scrap -from backend.module.utils import manage_image -from backend.models.model_cache import RequestCache -from core.settings import BASE_DIR -from backend.module.utils import cloudflare_turnstile - - -env = environ.Env() - - -@csrf_exempt -@ratelimit(key='ip', rate='20/m') -def get_list(request): - if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400) - token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN') - if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511) - - payload = json.loads(request.body) - search = payload.get("search") - page = payload.get("page") - source = payload.get("source") - - if search.get("text"): DATA = web_scrap.source_control[source].search.scrap(search=search,page=page) - else: DATA = web_scrap.source_control["colamanga"].get_list.scrap(page=page) - - return JsonResponse({"data":DATA}) - - -@ratelimit(key='ip', rate='20/m') -def search(request): - # if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400) - try: - DATA = web_scrap.source_control["colamanga"].search.scrap(search="妖") - return JsonResponse({"data":DATA}) - except Exception as e: - return HttpResponseBadRequest(str(e), status=500) - - - -@csrf_exempt -@ratelimit(key='ip', rate='20/m') -def get(request): - if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400) - token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN') - if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511) - - payload = json.loads(request.body) - id = payload.get("id") - source = payload.get("source") - - try: - DATA = web_scrap.source_control[source].get.scrap(id=id) - return JsonResponse({"data":DATA}) - except Exception as e: - - return HttpResponseBadRequest(str(e), status=500) - - -@ratelimit(key='ip', rate='60/m') -def get_cover(request,source,id,cover_id): - token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN') - if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511) - - try: - DATA = web_scrap.source_control[source].get_cover.scrap(id=id,cover_id=cover_id) - if not DATA: HttpResponseBadRequest('Image Not found!', status=404) - response = HttpResponse(DATA, content_type="image/png") - response['Content-Disposition'] = f'inline; filename="{id}-{cover_id}.png"' - return response - except Exception as e: - return HttpResponseBadRequest(str(e), status=500) - - \ No newline at end of file diff --git a/backend/api/web_scrape.py b/backend/api/web_scrape.py new file mode 100644 index 0000000000000000000000000000000000000000..2a95b613d9d5252ff4f1e7c3fd5e4d5a7734c406 --- /dev/null +++ b/backend/api/web_scrape.py @@ -0,0 +1,135 @@ + +import json, environ, requests, os, subprocess +import asyncio, uuid, shutil + +from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, StreamingHttpResponse +from django_ratelimit.decorators import ratelimit +from django.views.decorators.csrf import csrf_exempt +from asgiref.sync import sync_to_async + +from backend.module import web_scrap +from backend.module.utils import manage_image +from backend.models.model_cache import RequestCache +from core.settings import BASE_DIR +from backend.module.utils import cloudflare_turnstile +from backend.models.model_1 import WebscrapeGetCoverCache + +from backend.module.utils import directory_info, date_utils + + +env = environ.Env() + +STORAGE_DIR = os.path.join(BASE_DIR,"storage") +if not os.path.exists(STORAGE_DIR): os.makedirs(STORAGE_DIR) + +@csrf_exempt +@ratelimit(key='ip', rate='20/m') +def get_list(request): + if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400) + token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN') + if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511) + + payload = json.loads(request.body) + search = payload.get("search") + page = payload.get("page") + source = payload.get("source") + + + + + + + if search.get("text"): DATA = web_scrap.source_control[source].search.scrap(search=search,page=page) + else: DATA = web_scrap.source_control["colamanga"].get_list.scrap(page=page) + + return JsonResponse({"data":DATA}) + + + +@csrf_exempt +@ratelimit(key='ip', rate='20/m') +def get(request): + if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400) + token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN') + if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511) + + payload = json.loads(request.body) + id = payload.get("id") + source = payload.get("source") + + try: + DATA = web_scrap.source_control[source].get.scrap(id=id) + return JsonResponse({"data":DATA}) + except Exception as e: + + return HttpResponseBadRequest(str(e), status=500) + + +@ratelimit(key='ip', rate='60/m') +def get_cover(request,source,id,cover_id): + token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN') + if not cloudflare_turnstile.check(token): return HttpResponseBadRequest('Cloudflare turnstile token not existed or expired!', status=511) + + file_path = "" + file_name = "" + chunk_size = 8192 + MAX_COVER_STORAGE_SIZE = 10 * 1024 * 1024 * 1024 + + try: + query_result = WebscrapeGetCoverCache.objects.filter(source=source,comic_id=id,cover_id=cover_id).first() + if ( + query_result + and os.path.exists(query_result.file_path) + and query_result.datetime >= date_utils.utc_time().add(-5,'hour').get() + ): + file_path = query_result.file_path + file_name = os.path.basename(file_path) + + else: + if not os.path.exists(os.path.join(STORAGE_DIR,"covers")): os.makedirs(os.path.join(STORAGE_DIR,"covers")) + + while True: + storage_size = directory_info.GetDirectorySize(directory=os.path.join(STORAGE_DIR,"covers"),max_threads=5) + if (storage_size >= MAX_COVER_STORAGE_SIZE): + query_result = WebscrapeGetCoverCache.objects.order_by("datetime").first() + if (query_result): + file_path = query_result.file_path + if os.path.exists(file_path): shutil.rmtree(file_path) + WebscrapeGetCoverCache.objects.filter(file_path=query_result.file_path).delete() + else: + shutil.rmtree(os.path.join(STORAGE_DIR,"covers")) + break + else: break + print(storage_size) + + DATA = web_scrap.source_control[source].get_cover.scrap(id=id,cover_id=cover_id) + if not DATA: HttpResponseBadRequest('Image Not found!', status=404) + + file_path = os.path.join(STORAGE_DIR,"covers",f'{source}-{id}-{cover_id}.png') + file_name = os.path.basename(file_path) + + with open(file_path, "wb") as f: f.write(DATA) + + WebscrapeGetCoverCache( + file_path=file_path, + source=source, + comic_id=id, + cover_id=cover_id, + ).save() + + + + def file_iterator(): + with open(file_path, 'rb') as f: + while chunk := f.read(chunk_size): + yield chunk + + response = StreamingHttpResponse(file_iterator()) + response['Content-Type'] = 'application/octet-stream' + response['Content-Length'] = os.path.getsize(file_path) + response['Content-Disposition'] = f'attachment; filename="{file_name}"' + return response + except Exception as e: + return HttpResponseBadRequest(str(e), status=500) + + \ No newline at end of file diff --git a/backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc b/backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc index 2348e8e071c26024e2e071fd8809049015ead8b8..7754f3554b4dfce0c8845683cdaa072861fed54a 100644 Binary files a/backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc and b/backend/invoke_worker/__pycache__/chapter_queue.cpython-312.pyc differ diff --git a/backend/invoke_worker/chapter_queue.py b/backend/invoke_worker/chapter_queue.py index 3f799c347b10a000e9639fe5f1ecb12c7db96151..656b7d802fd34afe95fc201c5746232ac0244b5d 100644 --- a/backend/invoke_worker/chapter_queue.py +++ b/backend/invoke_worker/chapter_queue.py @@ -2,7 +2,8 @@ from django_thread import Thread from time import sleep from backend.module.utils import date_utils from django.db import connections -from backend.models.model_cache import SocketRequestChapterQueueCache, ComicStorageCache +from backend.models.model_cache import SocketRequestChapterQueueCache +from backend.models.model_1 import ComicStorageCache from core.settings import BASE_DIR from backend.module import web_scrap from backend.module.utils import manage_image @@ -22,6 +23,8 @@ env = environ.Env() STORAGE_DIR = os.path.join(BASE_DIR,"storage") if not os.path.exists(STORAGE_DIR): os.makedirs(STORAGE_DIR) +COMIC_STORAGE_DIR = os.path.join(STORAGE_DIR,"comics") +if not os.path.exists(COMIC_STORAGE_DIR): os.makedirs(COMIC_STORAGE_DIR) LOG_DIR = os.path.join(BASE_DIR, "log") if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR) @@ -37,7 +40,7 @@ class Job(Thread): query_result = SocketRequestChapterQueueCache.objects.order_by("datetime").first() while True: - if (GetDirectorySize(STORAGE_DIR) >= MAX_STORAGE_SIZE): + if (GetDirectorySize(COMIC_STORAGE_DIR) >= MAX_STORAGE_SIZE): query_result_2 = ComicStorageCache.objects.order_by("datetime").first() if (query_result_2): file_path = query_result_2.file_path @@ -73,33 +76,44 @@ class Job(Thread): else: connections['cache'].close() - script = [] - - input_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),"temp") + if (options.get("colorize") or options.get("translate").get("state")): + script = [] + input_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"temp") + merge_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"merged") - if (options.get("translate").get("state") and options.get("colorize")): - - managed_output_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated_colorized") - 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}"] - elif (options.get("translate").get("state") and not options.get("colorize")): - - managed_output_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated") - 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}"] - elif (options.get("colorize") and not options.get("translate").get("state")): - - managed_output_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),"colorized") - 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}"] + if (options.get("translate").get("state") and options.get("colorize")): + managed_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated_colorized") + 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"{merge_output_dir}", "-o", f"{managed_output_dir}" + ] + elif (options.get("translate").get("state") and not options.get("colorize")): + managed_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),f"{options.get("translate").get("target")}_translated") + 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"{merge_output_dir}", "-o", f"{managed_output_dir}" + ] + elif (options.get("colorize") and not options.get("translate").get("state")): + + managed_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"colorized") + script = [ + "python", "-m", "manga_translator", "-v", "--overwrite", "--attempts=3", "--detector=none", "--translator=original", "--colorize=mc2", "--colorization-size=-1", + "-i", f"{merge_output_dir}", "-o", f"{managed_output_dir}" + ] - if target_lang == "ENG": script.append("--manga2eng") + if target_lang == "ENG": script.append("--manga2eng") - - if (options.get("colorize") or options.get("translate").get("state")): if os.path.exists(input_dir): shutil.rmtree(input_dir) + if os.path.exists(merge_output_dir): shutil.rmtree(merge_output_dir) if os.path.exists(managed_output_dir): shutil.rmtree(managed_output_dir) job = web_scrap.source_control[source].get_chapter.scrap(comic_id=comic_id,chapter_id=chapter_id,output_dir=input_dir) + if job.get("status") == "success": + manage_image.merge_images_vertically(input_dir, merge_output_dir, max_height=1800) + shutil.rmtree(input_dir) + with open(os.path.join(LOG_DIR,"image_translator_output.log"), "w") as file: result = subprocess.run( @@ -113,7 +127,7 @@ class Job(Thread): ) if result.returncode != 0: raise Exception("Image Translator Execution error!") os.makedirs(managed_output_dir,exist_ok=True) - shutil.rmtree(input_dir) + shutil.rmtree(merge_output_dir) with zipfile.ZipFile(managed_output_dir + '.zip', 'w') as zipf: for foldername, subfolders, filenames in os.walk(managed_output_dir): @@ -139,63 +153,80 @@ class Job(Thread): query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first() channel_name = query_result_3.channel_name if query_result_3 else "" - channel_layer = get_channel_layer() - async_to_sync(channel_layer.send)(channel_name, { - 'type': 'event_send', - 'data': { - "type": "chapter_ready_to_download", - "data": { - "source": source, - "comic_id": comic_id, - "chapter_id": chapter_id, - "chapter_idx": chapter_idx - } - } - }) SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete() - else: raise Exception("Dowload chapter error!") + if channel_name: + channel_layer = get_channel_layer() + async_to_sync(channel_layer.send)(channel_name, { + 'type': 'event_send', + 'data': { + "type": "chapter_ready_to_download", + "data": { + "source": source, + "comic_id": comic_id, + "chapter_id": chapter_id, + "chapter_idx": chapter_idx + } + } + }) + connections['cache'].close() + + else: + connections['cache'].close() + raise Exception("#1 Dowload chapter error!") else: - input_dir = os.path.join(STORAGE_DIR,source,comic_id,str(chapter_idx),"original") + input_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"original") + merge_output_dir = os.path.join(COMIC_STORAGE_DIR,source,comic_id,str(chapter_idx),"origin-merged") + if os.path.exists(input_dir): shutil.rmtree(input_dir) + if os.path.exists(merge_output_dir): shutil.rmtree(merge_output_dir) job = web_scrap.source_control["colamanga"].get_chapter.scrap(comic_id=comic_id,chapter_id=chapter_id,output_dir=input_dir) - - with zipfile.ZipFile(input_dir + '.zip', 'w') as zipf: - for foldername, subfolders, filenames in os.walk(input_dir): - for filename in filenames: - if filename.endswith(('.png', '.jpg', '.jpeg')): - file_path = os.path.join(foldername, filename) - zipf.write(file_path, os.path.basename(file_path)) - shutil.rmtree(input_dir) - - ComicStorageCache( - source = source, - comic_id = comic_id, - chapter_id = chapter_id, - chapter_idx = chapter_idx, - file_path = input_dir + '.zip', - colorize = False, - translate = False, - target_lang = "", - - ).save() - query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first() - channel_name = query_result_3.channel_name if query_result_3 else "" - channel_layer = get_channel_layer() - async_to_sync(channel_layer.send)(channel_name, { - 'type': 'event_send', - 'data': { - "type": "chapter_ready_to_download", - "data": { - "source": source, - "comic_id": comic_id, - "chapter_id": chapter_id, - "chapter_idx": chapter_idx - } - } - }) - SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete() - + if job.get("status") == "success": + manage_image.merge_images_vertically(input_dir, merge_output_dir, max_height=1800) + if os.path.exists(input_dir): shutil.rmtree(input_dir) + + with zipfile.ZipFile(input_dir + '.zip', 'w') as zipf: + for foldername, subfolders, filenames in os.walk(merge_output_dir): + for filename in filenames: + if filename.endswith(('.png', '.jpg', '.jpeg')): + file_path = os.path.join(foldername, filename) + zipf.write(file_path, os.path.basename(file_path)) + shutil.rmtree(merge_output_dir) + + ComicStorageCache( + source = source, + comic_id = comic_id, + chapter_id = chapter_id, + chapter_idx = chapter_idx, + file_path = input_dir + '.zip', + colorize = False, + translate = False, + target_lang = "", + + ).save() + + + query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first() + channel_name = query_result_3.channel_name if query_result_3 else "" + SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete() + + if channel_name: + channel_layer = get_channel_layer() + async_to_sync(channel_layer.send)(channel_name, { + 'type': 'event_send', + 'data': { + "type": "chapter_ready_to_download", + "data": { + "source": source, + "comic_id": comic_id, + "chapter_id": chapter_id, + "chapter_idx": chapter_idx + } + } + }) + else: + connections['cache'].close() + raise Exception("#2 Dowload chapter error!") connections['cache'].close() else: connections['cache'].close() @@ -206,18 +237,24 @@ class Job(Thread): if os.path.exists(input_dir): shutil.rmtree(input_dir) if (managed_output_dir): if os.path.exists(managed_output_dir): shutil.rmtree(managed_output_dir) + + query_result_3 = SocketRequestChapterQueueCache.objects.filter(id=query_result.id).first() channel_name = query_result_3.channel_name if query_result_3 else "" - channel_layer = get_channel_layer() - async_to_sync(channel_layer.send)(channel_name, { - 'type': 'event_send', - 'data': { - "type": "chapter_ready_to_download", - "data": {"state":"error"} - } - }) - SocketRequestChapterQueueCache.objects.filter(id=query_result.id).delete() + + if (channel_name): + channel_layer = get_channel_layer() + async_to_sync(channel_layer.send)(channel_name, { + 'type': 'event_send', + 'data': { + "type": "chapter_ready_to_download", + "data": {"state":"error"} + } + }) + + + connections['cache'].close() sleep(10) diff --git a/backend/migrations/0001_initial.py b/backend/migrations/0001_initial.py index 3173af89ec0990bba1cc13ab3cbbf70778e0acdf..6fe8e6dd89c33b868201c814ecc59da388f8bec8 100644 --- a/backend/migrations/0001_initial.py +++ b/backend/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 5.1.1 on 2024-10-01 06:35 +# Generated by Django 5.1.1 on 2024-12-01 11:29 +import backend.models.model_1 import backend.models.model_cache import uuid from django.db import migrations, models @@ -32,13 +33,12 @@ class Migration(migrations.Migration): ('colorize', models.BooleanField()), ('translate', models.BooleanField()), ('target_lang', models.TextField()), - ('datetime', models.DateTimeField(default=backend.models.model_cache.get_current_utc_time)), + ('datetime', models.DateTimeField(default=backend.models.model_1.get_current_utc_time)), ], ), migrations.CreateModel( name='RequestCache', fields=[ - ('room', models.TextField()), ('client', models.UUIDField(primary_key=True, serialize=False)), ('datetime', models.DateTimeField(default=backend.models.model_cache.get_current_utc_time)), ], diff --git a/backend/migrations/0002_remove_requestcache_room.py b/backend/migrations/0002_remove_requestcache_room.py deleted file mode 100644 index 0b69dbb13bcf6d9e8037be9ea3553a71c0410946..0000000000000000000000000000000000000000 --- a/backend/migrations/0002_remove_requestcache_room.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-08 17:33 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('backend', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='requestcache', - name='room', - ), - ] diff --git a/backend/migrations/0002_webscrapegetcovercache.py b/backend/migrations/0002_webscrapegetcovercache.py new file mode 100644 index 0000000000000000000000000000000000000000..7a82b6e723acb1e915e4156ad52542829d45c472 --- /dev/null +++ b/backend/migrations/0002_webscrapegetcovercache.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.1 on 2024-12-01 11:38 + +import backend.models.model_1 +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('backend', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='WebscrapeGetCoverCache', + fields=[ + ('file_path', models.TextField(primary_key=True, serialize=False)), + ('source', models.TextField()), + ('comic_id', models.TextField()), + ('cover_id', models.TextField()), + ('datetime', models.DateTimeField(default=backend.models.model_1.get_current_utc_time)), + ], + ), + ] diff --git a/backend/migrations/__pycache__/0001_initial.cpython-312.pyc b/backend/migrations/__pycache__/0001_initial.cpython-312.pyc index a63691b5b0ad7a5aeda2b01484bc169a57988d18..c1717c9c702b3bcf5101659b8f8bb742a93afad7 100644 Binary files a/backend/migrations/__pycache__/0001_initial.cpython-312.pyc and b/backend/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/backend/migrations/__pycache__/0002_remove_requestcache_room.cpython-312.pyc b/backend/migrations/__pycache__/0002_remove_requestcache_room.cpython-312.pyc deleted file mode 100644 index 51368267155c1f42cf680800e67ad361608c89a2..0000000000000000000000000000000000000000 Binary files a/backend/migrations/__pycache__/0002_remove_requestcache_room.cpython-312.pyc and /dev/null differ diff --git a/backend/migrations/__pycache__/0002_webscrapegetcovercache.cpython-312.pyc b/backend/migrations/__pycache__/0002_webscrapegetcovercache.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9dee266ba775bb728c51be2875ae6c5bb0b5bc38 Binary files /dev/null and b/backend/migrations/__pycache__/0002_webscrapegetcovercache.cpython-312.pyc differ diff --git a/backend/models/__pycache__/model_1.cpython-312.pyc b/backend/models/__pycache__/model_1.cpython-312.pyc index 95b4e1103d7bf44c633e874a5f647557fac96ac7..4c595b3710840e33f4295b7a55a293db68fc7ae4 100644 Binary files a/backend/models/__pycache__/model_1.cpython-312.pyc and b/backend/models/__pycache__/model_1.cpython-312.pyc differ diff --git a/backend/models/__pycache__/model_cache.cpython-312.pyc b/backend/models/__pycache__/model_cache.cpython-312.pyc index e4a804e528555d04c6e1662a9eb06cf2ae6420c5..f00685e22b26262c0d9e0e827d1a3e4e3e5e262f 100644 Binary files a/backend/models/__pycache__/model_cache.cpython-312.pyc and b/backend/models/__pycache__/model_cache.cpython-312.pyc differ diff --git a/backend/models/model_1.py b/backend/models/model_1.py index 12df1f9631c4b61fb35b22e94e28d1339665b653..918b8c5e0443151dae5810ace0fdb332b91e8378 100644 --- a/backend/models/model_1.py +++ b/backend/models/model_1.py @@ -1,8 +1,28 @@ from django.db import models -# Create your models here. +from backend.module.utils import date_utils +from core.settings import BASE_DIR +import uuid, os -# class AdminAccount(models.Model): -# account = models.CharField(max_length=36,default="") -# password = models.CharField(max_length=36,default="") +def get_current_utc_time(): return date_utils.utc_time().get() + + + +class ComicStorageCache(models.Model): + id = models.UUIDField(primary_key = True, default = uuid.uuid4, editable = False) + source = models.TextField() + comic_id = models.TextField() + chapter_id = models.TextField() + chapter_idx = models.IntegerField() + file_path = models.TextField() + colorize = models.BooleanField() + translate = models.BooleanField() + target_lang = models.TextField() + datetime = models.DateTimeField(default=get_current_utc_time) +class WebscrapeGetCoverCache(models.Model): + file_path = models.TextField(primary_key=True) + source = models.TextField() + comic_id = models.TextField() + cover_id = models.TextField() + datetime = models.DateTimeField(default=get_current_utc_time) diff --git a/backend/models/model_cache.py b/backend/models/model_cache.py index 0b1ef4d026081f74f704786d69ebec84515b416f..8be5565bc62f4bc7656af980daedef04df5cb4e7 100644 --- a/backend/models/model_cache.py +++ b/backend/models/model_cache.py @@ -12,18 +12,6 @@ class CloudflareTurnStileCache(models.Model): token = models.TextField(primary_key=True) datetime = models.DateTimeField(default=get_current_utc_time) -class ComicStorageCache(models.Model): - id = models.UUIDField(primary_key = True, default = uuid.uuid4, editable = False) - source = models.TextField() - comic_id = models.TextField() - chapter_id = models.TextField() - chapter_idx = models.IntegerField() - file_path = models.TextField() - colorize = models.BooleanField() - translate = models.BooleanField() - target_lang = models.TextField() - datetime = models.DateTimeField(default=get_current_utc_time) - class SocketRequestChapterQueueCache(models.Model): id = models.UUIDField(primary_key = True, default = uuid.uuid4, editable = False) diff --git a/backend/module/utils/__pycache__/manage_image.cpython-312.pyc b/backend/module/utils/__pycache__/manage_image.cpython-312.pyc index f4edb6f8c409f755c747507eeb8a9800d0372c95..ddf3666d0ccebe6aaf67e78906ea4294070e2a5c 100644 Binary files a/backend/module/utils/__pycache__/manage_image.cpython-312.pyc and b/backend/module/utils/__pycache__/manage_image.cpython-312.pyc differ diff --git a/backend/module/utils/manage_image.py b/backend/module/utils/manage_image.py index 1b28280d7ba3f7490075fe8ff5701a8f7ab4074c..f0268a290b84140ec4a68ee73f529b05562d1766 100644 --- a/backend/module/utils/manage_image.py +++ b/backend/module/utils/manage_image.py @@ -1,7 +1,7 @@ import os from PIL import Image -def merge_images_vertically(input_dir, output_dir, max_size=10 * 1024 * 1024): +def merge_images_vertically_old(input_dir, output_dir, max_size=1800): os.makedirs(output_dir,exist_ok=True) 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): merged_image.save(os.path.join(output_dir, f"{merged_file_index}.png")) +def merge_images_vertically(input_dir, output_dir, max_height=1800): + os.makedirs(output_dir, exist_ok=True) + + filenames = sorted(os.listdir(input_dir), key=lambda x: int(x.split(".")[0])) + + merged_image = None + merged_file_index = 0 + + index = 0 + while True: + if index > (len(filenames) - 1): break + file = filenames[index] + if not merged_image: + image = Image.open(os.path.join(input_dir, file)) + + width, height = image.size + + new_image = Image.new("RGBA", (width, height)) + + # Paste the image onto the new image + new_image.paste(image, (0, 0)) + merged_image = new_image + index += 1 + else: + merged_width, merged_height = merged_image.size + + if merged_height >= max_height: + output_path = os.path.join(output_dir, f"{merged_file_index}.png") + merged_image.save(output_path) + merged_image = None + merged_file_index += 1 + else: + image = Image.open(os.path.join(input_dir, file)) + width, height = image.size + + # Create a new image with the combined width and the height of the tallest image + new_width = max(merged_width, width) + new_height = merged_height + height + new_image = Image.new("RGB", (new_width, new_height)) + + # Paste the two images onto the new image + new_image.paste(merged_image, (0, 0)) + new_image.paste(image, (0, merged_height)) + merged_image = new_image + index += 1 + + if merged_image: + output_path = os.path.join(output_dir, f"{merged_file_index}.png") + merged_image.save(output_path) + + def split_image_vertically(input_dir, output_dir): os.makedirs(output_dir,exist_ok=True) diff --git a/backend/module/web_scrap/__pycache__/utils.cpython-312.pyc b/backend/module/web_scrap/__pycache__/utils.cpython-312.pyc index 56608c600be619f7094bf2a43cdb22d9176e1a2d..5c84d1c924712d77adf9808c304698a34b495fbe 100644 Binary files a/backend/module/web_scrap/__pycache__/utils.cpython-312.pyc and b/backend/module/web_scrap/__pycache__/utils.cpython-312.pyc differ diff --git a/backend/module/web_scrap/utils.py b/backend/module/web_scrap/utils.py index a0e1bc0cc541c775276cefcd3c21b674c54ecf0c..b10f86906c76899edf655a095cfb43ea66cc247f 100644 --- a/backend/module/web_scrap/utils.py +++ b/backend/module/web_scrap/utils.py @@ -17,7 +17,6 @@ class SeleniumScraper: options.add_argument('--no-sandbox') options.add_argument("--no-quit") options.add_argument('--disable-extensions') - options.add_argument('--disable-gpu') options.add_argument('--disable-dev-shm-usage') options.set_capability('goog:loggingPrefs', {'performance': 'ALL'}) self.__driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options) diff --git a/backend/urls.py b/backend/urls.py index 4b9f2b033c9b1fb8c873c18967a57602ec7084a4..c3bcf93f9d60be546c392714afe2a09ad2aafcd8 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.urls import path, include, re_path -from backend.api import test, stream_file, web_scrap, cloudflare_turnstile, queue +from backend.api import test, stream_file, cloudflare_turnstile, queue, web_scrape @@ -14,10 +14,9 @@ urlpatterns = [ # /api/queue/request_info/ path("cloudflare_turnstile/verify/", cloudflare_turnstile.verify), - path('web_scrap/get_list/', web_scrap.get_list), - path('web_scrap/search/', web_scrap.search), - path('web_scrap/get/', web_scrap.get), - path('web_scrap/get_cover////', web_scrap.get_cover), + path('web_scrap/get_list/', web_scrape.get_list), + path('web_scrap/get/', web_scrape.get), + path('web_scrap/get_cover////', web_scrape.get_cover), diff --git a/core/__pycache__/settings.cpython-312.pyc b/core/__pycache__/settings.cpython-312.pyc index 9ca512f782dc91c586a06dde03bec1baa43f9035..f9ca4c0d66066fefc89cb45e68e190a4d3df40c3 100644 Binary files a/core/__pycache__/settings.cpython-312.pyc and b/core/__pycache__/settings.cpython-312.pyc differ diff --git a/core/settings.py b/core/settings.py index 950519f65a94e433260cf0ad968b01fda68fecc7..138c778ef54cb00f2bdc2098892954300dfe4d14 100644 --- a/core/settings.py +++ b/core/settings.py @@ -21,8 +21,7 @@ for key, value in os.environ.items(): # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True -DEBUG_DB = True +DEBUG = False # settings.py @@ -117,37 +116,27 @@ WSGI_APPLICATION = 'core.wsgi.application' # https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASE_ROUTERS = ['core.routers.Router'] -if DEBUG or DEBUG_DB: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'database.sqlite3', - }, - 'cache': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'cache.sqlite3', - }, - 'DB1': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db1.sqlite3', - }, - 'DB2': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db2.sqlite3', - }, - } -else: - DATABASES = { - 'default': dj_database_url.config(default=env("DB")), - 'cache': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'cache.sqlite3', - }, - 'DB1': dj_database_url.config(default=env("DB1")), - 'DB2': dj_database_url.config(default=env("DB2")), - - } +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'database.sqlite3', + }, + 'cache': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'cache.sqlite3', + }, + 'DB1': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db1.sqlite3', + }, + 'DB2': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db2.sqlite3', + }, + +} + diff --git a/frontend/app/_layout.tsx b/frontend/app/_layout.tsx index c88123e5c5d34934353269a1a8a6bc2d138032f5..47dbd7e68475d3744e1f5400621a9525cdbabf0f 100644 --- a/frontend/app/_layout.tsx +++ b/frontend/app/_layout.tsx @@ -3,7 +3,7 @@ import { useFonts } from 'expo-font'; import { Stack, router, usePathname } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import { useEffect, useState, useContext, createContext, memo } from 'react'; -import { useWindowDimensions, View, Text, Pressable } from 'react-native'; +import { useWindowDimensions, View, Text, Pressable, KeyboardAvoidingView } from 'react-native'; import 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import Menu from '@/components/menu/menu'; @@ -134,7 +134,15 @@ return (<>{loaded && themeTypeContext && apiBaseContext && socketBaseContext && widgetContext, setWidgetContext, showCloudflareTurnstileContext, setShowCloudflareTurnstileContext, }}> - + {showCloudflareTurnstileContext ? {loaded && themeTypeContext && apiBaseContext && socketBaseContext && } - + diff --git a/frontend/app/bookmark/_layout.tsx b/frontend/app/bookmark/_layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..640fcb40c6e7aaeb7d70b0aeb03acdcf6da1b9e3 --- /dev/null +++ b/frontend/app/bookmark/_layout.tsx @@ -0,0 +1,19 @@ +import { Tabs, Stack } from 'expo-router'; +import React from 'react'; +import {View, Text} from 'react-native'; + +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import { SafeAreaView } from 'react-native-safe-area-context'; +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + + +); +} diff --git a/frontend/app/bookmark/components/bookmark_component.tsx b/frontend/app/bookmark/components/bookmark_component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2fc97bcbfddb0daf1d3e3838aba1e6bdba612580 --- /dev/null +++ b/frontend/app/bookmark/components/bookmark_component.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react'; +import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router'; +import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper'; +import CircularProgress from 'react-native-circular-progress-indicator'; +import { ActivityIndicator } from 'react-native-paper'; +import { FlashList } from "@shopify/flash-list"; + + +import uuid from 'react-native-uuid'; +import Toast from 'react-native-toast-message'; +import { View, AnimatePresence } from 'moti'; +import * as Clipboard from 'expo-clipboard'; +import * as FileSystem from 'expo-file-system'; +import NetInfo from "@react-native-community/netinfo"; +import { Marquee } from '@animatereactnative/marquee'; +import { Slider } from '@rneui/themed-edge'; + +import { __styles } from '../stylesheet/styles'; +import Storage from '@/constants/module/storages/storage'; +import ChapterStorage from '@/constants/module/storages/chapter_storage'; +import Image from '@/components/Image'; +import {CONTEXT} from '@/constants/module/context'; +import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager"; +import Theme from '@/constants/theme'; +import ComicStorage from '@/constants/module/storages/comic_storage'; + +const BookmarkComponent = ({item, SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK}:any) => { + const Dimensions = useWindowDimensions(); + const controller = new AbortController(); + const signal = controller.signal; + + const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) + const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) + const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + const {widgetContext, setWidgetContext}:any = useContext(CONTEXT) + const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) + + + useFocusEffect(useCallback(() => { + + + return () => { + controller.abort(); + }; + },[])) + + return (<> + {SET_SELECTED_BOOKMARK(item)}} + > + + + {item} + + + + ) + + +} + +export default BookmarkComponent; + diff --git a/frontend/app/bookmark/components/comic_component.tsx b/frontend/app/bookmark/components/comic_component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..75de60fbe244bab06fc198e9cccae4540d61202b --- /dev/null +++ b/frontend/app/bookmark/components/comic_component.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react'; +import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router'; +import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper'; +import CircularProgress from 'react-native-circular-progress-indicator'; +import { ActivityIndicator } from 'react-native-paper'; +import { FlashList } from "@shopify/flash-list"; + + +import uuid from 'react-native-uuid'; +import Toast from 'react-native-toast-message'; +import { View, AnimatePresence } from 'moti'; +import * as Clipboard from 'expo-clipboard'; +import * as FileSystem from 'expo-file-system'; +import NetInfo from "@react-native-community/netinfo"; +import { Marquee } from '@animatereactnative/marquee'; +import { Slider } from '@rneui/themed-edge'; + +import { __styles } from '../stylesheet/styles'; +import Storage from '@/constants/module/storages/storage'; +import ChapterStorage from '@/constants/module/storages/chapter_storage'; +import CoverStorage from '@/constants/module/storages/cover_storage'; + +import Image from '@/components/Image'; +import {CONTEXT} from '@/constants/module/context'; +import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager"; +import Theme from '@/constants/theme'; +import ComicStorage from '@/constants/module/storages/comic_storage'; + +const ComicComponent = ({item, SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK}:any) => { + const Dimensions = useWindowDimensions(); + const controller = new AbortController(); + const signal = controller.signal; + + const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) + const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) + const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + const {widgetContext, setWidgetContext}:any = useContext(CONTEXT) + const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) + + const [styles, setStyles]:any = useState(null) + const [isLoading, setIsLoading] = useState(true) + + const cover:any = useRef("") + + useFocusEffect(useCallback(() => { + (async ()=>{ + setIsLoading(true) + setStyles(__styles(themeTypeContext,Dimensions)) + const stored_bookmark = await Storage.get("bookmark") || [] + console.log(stored_bookmark) + cover.current = await CoverStorage.get(`${item.source}-${item.id}`) || "" + setIsLoading(false) + })() + + return () => { + cover.current = "" + controller.abort(); + }; + },[])) + + return (<>{styles && !isLoading && <> + {router.navigate(`/view/${item.source}/${item.id}/?mode=local`)}} + style={styles.item_box} + > + <> + {console.log("load image error",error)}} source={cover.current} + style={styles.item_cover} + contentFit="cover" transition={1000} + onLoadEnd={()=>{cover.current = ""}} + /> + + {item.info.title} + + + }) + + +} + +export default ComicComponent; + diff --git a/frontend/app/bookmark/components/widgets/bookmark.tsx b/frontend/app/bookmark/components/widgets/bookmark.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6dfee6f2c98b053368ed88dac7a8ccc0357d1da --- /dev/null +++ b/frontend/app/bookmark/components/widgets/bookmark.tsx @@ -0,0 +1,904 @@ + +import React, { useEffect, useState, useCallback, useContext, useRef, Fragment, memo } from 'react'; +import { Platform, useWindowDimensions, ScrollView } from 'react-native'; + +import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper'; +import { View, AnimatePresence } from 'moti'; +import Toast from 'react-native-toast-message'; +import * as FileSystem from 'expo-file-system'; +import axios from 'axios'; + + +import Theme from '@/constants/theme'; +import Dropdown from '@/components/dropdown'; +import { CONTEXT } from '@/constants/module/context'; +import Storage from '@/constants/module/storages/storage'; +import ComicStorage from '@/constants/module/storages/comic_storage'; +import ImageCacheStorage from '@/constants/module/storages/image_cache_storage'; +import ChapterStorage from '@/constants/module/storages/chapter_storage'; +import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage'; + +interface BookmarkWidgetProps { + setIsLoading: any; + onRefresh: any; +} + +const BookmarkWidget: React.FC = ({ + setIsLoading, + onRefresh, +}) => { + const Dimensions = useWindowDimensions(); + + const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) + const {widgetContext, setWidgetContext}:any = useContext(CONTEXT) + const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) + const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + + const [BOOKMARK_DATA, SET_BOOKMARK_DATA]: any = useState([]) + const [MIGRATE_BOOKMARK_DATA, SET_MIGRATE_BOOKMARK_DATA]:any = useState([]) + + + const [showMenuOption, setShowMenuOption]:any = useState({state:false,positions:[0,0,0,0],id:""}) + const [searchTag, setSearchTag]:any = useState("") + + const [migrateTag,setMigrateTag]:any = useState("") + + const [manageBookmark, setManageBookmark]:any = useState({edit:"",delete:""}) + const [createTag, setCreateTag]:any = useState({state:false,title:""}) + const [removeTag, setRemoveTag]:any = useState({state:false, removing: false}) + + + const controller = new AbortController(); + const signal = controller.signal; + + const RenderTag = useCallback(({item}:any) =>{ + const [editTag, setEditTag]:any = useState(item.value) + useEffect(()=>{ + },[manageBookmark]) + + return (<> + {item.value.includes(searchTag) && + ( + + <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value && + ( + {item.label} + + + { + if (manageBookmark.edit){ + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + } + + const x = event.nativeEvent.pageX + const y = event.nativeEvent.pageY + + setShowMenuOption({ + ...showMenuOption, + state: showMenuOption.id === item.value ? false : true, + positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0], + id:showMenuOption.id === item.value ? "" : item.value, + }) + + + + }} + > + + + + + ) + } + <>{manageBookmark.edit === item.value && + ( + + } + style={{ + backgroundColor:Theme[themeTypeContext].background_color, + borderColor:Theme[themeTypeContext].border_color, + + }} + outlineColor={Theme[themeTypeContext].text_input_border_color} + value={editTag} + onChangeText={(text)=>{ + setEditTag(text) + }} + /> + + + { + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + setShowMenuOption({...showMenuOption,state:false,id:""}) + }} + > + + + + { + const stored_bookmark = await Storage.get("bookmark"); + + const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit); + + if (index !== -1){ + stored_bookmark[index] = editTag; + await Storage.store("bookmark", stored_bookmark) + + const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit) + for (const item of stored_comics){ + await ComicStorage.replaceTag(item.source, item.id, editTag) + } + + + + + const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit); + if (index_2 !== -1){ + BOOKMARK_DATA[index_2].label = editTag + BOOKMARK_DATA[index_2].value = editTag + } + SET_BOOKMARK_DATA(BOOKMARK_DATA) + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + + onRefresh(); + } + setShowMenuOption({...showMenuOption,state:false,id:""}) + }} + > + + + + + + + + ) + + } + + + + + + + ) + } + ) + } + ,[Theme,themeTypeContext,manageBookmark,searchTag,removeTag,createTag,showMenuOption,MIGRATE_BOOKMARK_DATA,BOOKMARK_DATA]) + + + const load_bookmark = async ()=>{ + const stored_bookmark_data = await Storage.get("bookmark") || [] + if (stored_bookmark_data.length) { + const bookmark_data:Array = [] + for (const item of stored_bookmark_data) { + bookmark_data.push({ + label:item, + value:item, + }) + } + + SET_BOOKMARK_DATA(bookmark_data.sort()) + }else SET_BOOKMARK_DATA([]) + } + + useEffect(()=>{ + SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA]) + },[BOOKMARK_DATA]) + + useEffect(()=>{ + load_bookmark() + return () => controller.abort(); + },[]) + + return (<>{BOOKMARK_DATA !== null && <> + + + + <>{!createTag.state && !removeTag.state && <> + + <>{BOOKMARK_DATA.length + ? <> + + { + setSearchTag(event.nativeEvent.text) + }} + /> + + + + <>{BOOKMARK_DATA.map((item:any) => + ( + + + + + ) + )} + + + + : <> + No tag found + + + } + + + + + + + } + + <>{createTag.state && + <> + + } + style={{ + + backgroundColor:Theme[themeTypeContext].background_color, + borderColor:Theme[themeTypeContext].border_color, + + }} + outlineColor={Theme[themeTypeContext].text_input_border_color} + value={createTag.title} + onChange={(event)=>{ + setCreateTag({...createTag,title:event.nativeEvent.text}) + }} + /> + + + + + + + } + + + <>{showMenuOption.state && + + { + setManageBookmark({...manageBookmark,edit:showMenuOption.id}) + setShowMenuOption({...showMenuOption,state:false}) + }} + > + + + + Edit + + + + + { + setManageBookmark({...manageBookmark,edit:"",delete:showMenuOption.id}) + setShowMenuOption({...showMenuOption,state:false}) + }} + > + + + + Delete + + + + + + + } + <>{manageBookmark.delete && ( + + + + + + Delete Tag: "{manageBookmark.delete}" + + + + item.value !== manageBookmark.delete)} + value={migrateTag} + onChange={(async (item:any) => { + setMigrateTag(item.value) + })} + /> + + <>{!migrateTag && ( + Setting migration to None will remove all comics and chapters for this bookmark tag. + )} + + <>{migrateTag + + ? { + const stored_bookmark = await Storage.get("bookmark") + const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete) + if (index === -1) return + + const stored_comics = await ComicStorage.getByTag(manageBookmark.delete) + for (const comic of stored_comics) { + const source = comic.source; + const comic_id = comic.id + await ComicStorage.replaceTag(source,comic_id,migrateTag) + + } + + stored_bookmark.splice(index, 1); + await Storage.store("bookmark",stored_bookmark); + + + + + const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete); + if (index_2 !== -1){ + BOOKMARK_DATA.splice(index_2, 1); + } + SET_BOOKMARK_DATA(BOOKMARK_DATA) + setManageBookmark({...manageBookmark,edit:"",delete:""}) + setShowMenuOption({...showMenuOption,state:false,id:""}) + setMigrateTag("") + + onRefresh(); + }} + > + + Migrate + + + : { + const stored_bookmark = await Storage.get("bookmark"); + const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete) + if (index === -1) return + await ComicStorage.removeByTag(manageBookmark.delete); + stored_bookmark.splice(index, 1); + await Storage.store("bookmark",stored_bookmark); + + const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete); + if (index_2 !== -1){ + BOOKMARK_DATA.splice(index_2, 1); + } + SET_BOOKMARK_DATA(BOOKMARK_DATA) + setManageBookmark({...manageBookmark,edit:"",delete:""}) + setShowMenuOption({...showMenuOption,state:false,id:""}) + setMigrateTag("") + onRefresh() + + }} + > + + Delete + + + } + { + setShowMenuOption({...showMenuOption,state:false,id:""}) + setManageBookmark({...manageBookmark,edit:"",delete:""}) + }} + > + + Cancel + + + + + + + + + )} + + }) +} + +export default BookmarkWidget; \ No newline at end of file diff --git a/frontend/app/bookmark/index.tsx b/frontend/app/bookmark/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d5171f19bfefe01710d677973adf6e2d3d9fd4ab --- /dev/null +++ b/frontend/app/bookmark/index.tsx @@ -0,0 +1,291 @@ +import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react'; +import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router'; +import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper'; +import CircularProgress from 'react-native-circular-progress-indicator'; +import { ActivityIndicator } from 'react-native-paper'; +import { FlashList } from "@shopify/flash-list"; + + +import uuid from 'react-native-uuid'; +import Toast from 'react-native-toast-message'; +import { View, AnimatePresence } from 'moti'; +import * as Clipboard from 'expo-clipboard'; +import * as FileSystem from 'expo-file-system'; +import NetInfo from "@react-native-community/netinfo"; +import { Marquee } from '@animatereactnative/marquee'; +import { Slider } from '@rneui/themed-edge'; + +import BookmarkComponent from './components/bookmark_component'; +import ComicComponent from './components/comic_component'; +import BookmarkWidget from './components/widgets/bookmark'; + +import { __styles } from './stylesheet/styles'; +import Storage from '@/constants/module/storages/storage'; +import ChapterStorage from '@/constants/module/storages/chapter_storage'; +import Image from '@/components/Image'; +import {CONTEXT} from '@/constants/module/context'; +import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager"; +import Theme from '@/constants/theme'; +import ComicStorage from '@/constants/module/storages/comic_storage'; + +const Index = ({}:any) => { + const Dimensions = useWindowDimensions(); + const controller = new AbortController(); + const signal = controller.signal; + + const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) + const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) + const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + const {widgetContext, setWidgetContext}:any = useContext(CONTEXT) + const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) + + const [styles, setStyles]:any = useState("") + const [isLoading, setIsLoading] = useState(true) + const [onRefresh, setOnRefresh] = useState(false) + const [search, setSearch] = useState({state:false,text:""}) + + const [BOOKMARK_DATA, SET_BOOKMARK_DATA]:any = useState([]) + const [SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK] = useState("") + + const [COMIC_DATA, SET_COMIC_DATA] = useState([]) + + useEffect(() => { + (async ()=>{ + if (!SELECTED_BOOKMARK) return + const stored_comic = await ComicStorage.getByTag(SELECTED_BOOKMARK) + console.log(stored_comic) + SET_COMIC_DATA(stored_comic) + })() + },[SELECTED_BOOKMARK,onRefresh]) + + useFocusEffect(useCallback(() => { + (async ()=>{ + setIsLoading(true) + const stored_bookmark = await Storage.get("bookmark") || [] + console.log(stored_bookmark) + + SET_BOOKMARK_DATA(stored_bookmark) + console.log("AA",stored_bookmark.length ) + if (stored_bookmark.length) { + SET_SELECTED_BOOKMARK(stored_bookmark[0]) + } + setIsLoading(false) + })() + },[onRefresh])) + + const renderBookmarkComponent = useCallback(({item,index}:any) => { + return + },[SELECTED_BOOKMARK]) + + const renderComicComponent = useCallback(({item,index}:any) => { + return + },[SELECTED_BOOKMARK]) + + useFocusEffect(useCallback(() => { + setIsLoading(true) + setShowMenuContext(true) + setStyles(__styles(themeTypeContext,Dimensions)) + + return () => { + controller.abort(); + }; + },[])) + + return (<>{styles && ! isLoading + ? <> + + + Bookmark + + + + { + setSearch({...search,state:!search.state}) + }} + > + + + + + + + <>{search.state && ( + + { + setSearch({...search,text:event.nativeEvent.text}) + }} + /> + + + )} + + + { + setWidgetContext({state:true,component: + {setOnRefresh(!onRefresh)}} + + /> + }) + }} + > + + + + item.info.title.toLowerCase().includes(search.text.toLowerCase()))} + ListEmptyComponent={ + + <>{BOOKMARK_DATA.length + ? <> + {search.text && COMIC_DATA.length + ? <> + + Search no result + + : <> + + This tag is empty. + + } + + : + + + No tag found. {"\n"}Press{" "} + + {" "}to create bookmark tag. + + + + } + + + + + } + /> + + + + : + + + }) + + +} + +export default Index; + diff --git a/frontend/app/bookmark/stylesheet/styles.tsx b/frontend/app/bookmark/stylesheet/styles.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0daa4ac6af117f1b55aade34b399d1b62a2af01a --- /dev/null +++ b/frontend/app/bookmark/stylesheet/styles.tsx @@ -0,0 +1,68 @@ +import { StyleSheet } from "react-native"; +import Theme from "@/constants/theme"; + +export const __styles:any = (theme_type:string,Dimensions:any) => { + return StyleSheet.create({ + screen_container: { + display: "flex", + width: "100%", + height: "100%", + backgroundColor: Theme[theme_type].background_color, + }, + header_container: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 15, + paddingVertical:10, + backgroundColor: Theme[theme_type].background_color, + borderBottomWidth: 0.5, + borderColor: Theme[theme_type].border_color, + }, + header_text:{ + fontFamily: "roboto-medium", + fontSize: ((Dimensions.width+Dimensions.height)/2)*0.04, + color: Theme[theme_type].text_color, + + }, + header_search_button:{ + borderRadius:5, + borderWidth:0, + padding:5, + }, + + item_box:{ + display:"flex", + flexDirection:"column", + alignItems:"center", + gap:15, + height:"auto", + width:Math.max(((Dimensions.width+Dimensions.height)/2)*0.225,100), + borderRadius:8, + + }, + item_cover:{ + width:"100%", + height:Math.max(((Dimensions.width+Dimensions.height)/2)*0.325,125), + borderRadius:8, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.22, + shadowRadius: 2.22, + + elevation: 3, + }, + item_title:{ + color: Theme[theme_type].text_color, + fontFamily: "roboto-medium", + fontSize: ((Dimensions.width+Dimensions.height)/2)*0.025, + width:"100%", + height:"auto", + textAlign:"center", + flexShrink:1, + } + })} \ No newline at end of file diff --git a/frontend/app/explore/index.tsx b/frontend/app/explore/index.tsx index 981deb4546ddd56a95690a6fa0a32d0f54011f00..90241d6febed10e9218bf17af6f4466ea5d2e1bc 100644 --- a/frontend/app/explore/index.tsx +++ b/frontend/app/explore/index.tsx @@ -9,7 +9,7 @@ import Dropdown from '@/components/dropdown'; import Theme from '@/constants/theme'; -import { __styles } from './stylesheet/show_list_styles'; +import { __styles } from './stylesheet/styles'; import Storage from '@/constants/module/storages/storage'; import ImageStorage from '@/constants/module/storages/image_cache_storage'; import { CONTEXT } from '@/constants/module/context'; @@ -52,7 +52,7 @@ const Index = ({}:any) => { }; }, [])) - useEffect(() => { + useFocusEffect(useCallback(() => { (async ()=>{ setStyles(__styles(themeTypeContext,Dimensions)) @@ -69,7 +69,7 @@ const Index = ({}:any) => { return () => { controller.abort(); }; - },[]) + },[])) @@ -397,7 +397,7 @@ const Index = ({}:any) => { contentFit="cover" transition={1000} /> - {item.title} + {item.title} diff --git a/frontend/app/explore/stylesheet/show_list_styles.tsx b/frontend/app/explore/stylesheet/styles.tsx similarity index 100% rename from frontend/app/explore/stylesheet/show_list_styles.tsx rename to frontend/app/explore/stylesheet/styles.tsx diff --git a/frontend/app/index.tsx b/frontend/app/index.tsx index a21e21dbd820efe195d6adb155f0278e7f559140..0fbfde4c9dd6330e9e330f7a5f04f8886fb649c8 100644 --- a/frontend/app/index.tsx +++ b/frontend/app/index.tsx @@ -9,7 +9,7 @@ const Index = () => { const pathname = usePathname() if (pathname === "/" || pathname === "") return ( - + ) } diff --git a/frontend/app/read/[source]/[comic_id]/[chapter_idx].tsx b/frontend/app/read/[source]/[comic_id]/[chapter_idx].tsx index d0a3d6010e8060e456588c042add85605f157dc2..06ac60975d09acec2192c8ad080075b9dae210e0 100644 --- a/frontend/app/read/[source]/[comic_id]/[chapter_idx].tsx +++ b/frontend/app/read/[source]/[comic_id]/[chapter_idx].tsx @@ -16,6 +16,7 @@ import * as FileSystem from 'expo-file-system'; import NetInfo from "@react-native-community/netinfo"; import { Marquee } from '@animatereactnative/marquee'; import { Slider } from '@rneui/themed-edge'; +import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); import Storage from '@/constants/module/storages/storage'; import ChapterStorage from '@/constants/module/storages/chapter_storage'; @@ -28,12 +29,12 @@ import Menu from '../../components/menu/menu'; import Disqus from '../../components/disqus'; import { get_chapter } from '../../modules/get_chapter'; import ComicStorage from '@/constants/module/storages/comic_storage'; +import { set } from 'lodash'; const Index = ({}:any) => { const SOURCE = useLocalSearchParams().source; const COMIC_ID = useLocalSearchParams().comic_id; const Dimensions = useWindowDimensions(); - const StaticDimensions = useMemo(() => Dimensions, []) const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) @@ -46,9 +47,11 @@ const Index = ({}:any) => { const [chapterInfo, setChapterInfo]:any = useState({}) const [showOptions, setShowOptions]:any = useState({type:"general",state:false}) const [DATA, SET_DATA]:any = useState([]) - const [isAdding, setIsAdding]:any = useState(false) + + const [zoom, setZoom]:any = useState(0) + const CHAPTER_IDX = useRef(Number(useLocalSearchParams().chapter_idx as string)); @@ -60,6 +63,27 @@ const Index = ({}:any) => { // First Load useEffect(()=>{(async () => { + setIsLoading(true) + + const stored_recent = await Storage.get("RECENT") || [] + stored_recent.sort((a:any,b:any) => b.timestamp - a.timestamp) + + let exist = false + for (const i of stored_recent) { + if (i.source === SOURCE && i.comic_id === COMIC_ID) { + i.timestamp = dayjs().utc().valueOf() + exist = true + break + } + } + if (!exist) { + if (stored_recent.length >= 25) stored_recent.pop() + stored_recent.push({source:SOURCE,comic_id:COMIC_ID,timestamp:dayjs().utc().valueOf()}) + } + + await Storage.store("RECENT",stored_recent) + + if (!SOURCE || !COMIC_ID || !CHAPTER_IDX.current){ setIsError({state:true,text:"Invalid source, comic id or chapter idx!"}) return @@ -94,39 +118,8 @@ const Index = ({}:any) => { const renderItem = useCallback(({item,index}:any) => { return - },[zoom,showOptions,setShowOptions]) - - const onViewableItemsChanged = useCallback(async ({viewableItems, changed}:any) => { - // const expect_chapter_idx = [CHAPTER_IDX.current + 1, CHAPTER_IDX.current - 1] - // const current_count = viewableItems.filter((data:any) => data.item.chapter_idx === CHAPTER_IDX.current).length - // const existed_count = viewableItems.filter((data:any) => expect_chapter_idx.includes(data.item.chapter_idx)).length - - // if (current_count || existed_count){ - // 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 - // if (choose_idx === CHAPTER_IDX.current) return - // const stored_chapter = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,choose_idx, {exclude_fields:["data"]}) - // setChapterInfo({ - // chapter_id: stored_chapter?.id, - // chapter_idx: stored_chapter?.id, - // title: stored_chapter?.title, - // }) - // const stored_comic = await ComicStorage.getByID(SOURCE, COMIC_ID) - // if (stored_comic.history.idx && choose_idx > stored_comic.history.idx) { - // await ComicStorage.updateHistory(SOURCE, COMIC_ID, {idx:stored_chapter?.idx,id:stored_chapter?.id,title:stored_chapter?.title}) - // } - // router.setParams({idx:choose_idx}) - // CHAPTER_IDX.current = choose_idx - // } - },[]) - - const onEndReached = useCallback(async () => { - console.log(DATA) - // const NEW_DATA = DATA.filter((data:any) => data.chapter_idx === CHAPTER_IDX.current-2) - - // const chapter_current_data = await get_chapter(SOURCE,COMIC_ID,CHAPTER_IDX.current+1) - - // SET_DATA([...NEW_DATA,...chapter_current_data]) - },[DATA]) + },[zoom,showOptions]) + return (<> {isError.state @@ -167,7 +160,7 @@ const Index = ({}:any) => { }} onPress={()=>{ - router.replace("/explore") + router.replace(`/view/${SOURCE}/${COMIC_ID}/?mode=local`) }} > @@ -220,18 +213,15 @@ const Index = ({}:any) => { zIndex:0 }} > - - {showOptions.state && + {showOptions.state && { }} onPress={()=>{ - router.replace(`/view/${SOURCE}/${COMIC_ID}/`) + router.replace(`/view/${SOURCE}/${COMIC_ID}/?mode=local`) }} > @@ -325,21 +315,7 @@ const Index = ({}:any) => { {chapterInfo.title} - { - console.log("HO2h2") - }} - > - - + { +const ChapterImage = ({item, zoom, showOptions, setShowOptions, setIsLoading, SET_DATA}:any)=>{ const SOURCE = useLocalSearchParams().source; const COMIC_ID = useLocalSearchParams().comic_id; const CHAPTER_IDX = Number(useLocalSearchParams().chapter_idx as string); @@ -34,6 +35,7 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET const [isReady, setIsReady] = useState(false); const [isError, setIsError] = useState({state:false,text:""}); + const [isNavigate, setIsNavigate] = useState(false); const image = useRef(null); const image_layout = useRef(null); @@ -53,9 +55,17 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET })()},[]) + useFocusEffect(useCallback(() => { + return () => { + image.current = null + }; + },[])) + return ( {setShowOptions({type:"general",state:!showOptions.state})}} + onPress={()=>{ + setShowOptions({type:"general",state:!showOptions.state}) + }} style={{ display:"flex", width:"100%", @@ -76,6 +86,7 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET aspectRatio: image_layout.current.width / image_layout.current.height, }} onLoadEnd={()=>{ + image.current = "" }} /> )} @@ -171,7 +182,7 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET paddingVertical: 18, }} > - { + setIsNavigate(true); const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx-1) if (stored_chapter_info?.data_state === "completed"){ + setIsLoading(true); router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`) }else{ Toast.show({ @@ -214,18 +227,19 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET }, }); } + setIsNavigate(false); }} > Previous - { + setIsNavigate(true); const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx+1) if (stored_chapter_info?.data_state === "completed"){ + setIsLoading(true) + + const stored_comic = await ComicStorage.getByID(SOURCE, COMIC_ID) + if (stored_comic.history.idx && item.chapter_idx+1 > stored_comic.history.idx) { + await ComicStorage.updateHistory(SOURCE, COMIC_ID, {idx:stored_chapter_info?.idx, id:stored_chapter_info?.id, title:stored_chapter_info?.title}) + } + router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`) }else{ Toast.show({ @@ -267,13 +289,14 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET }, }); } + setIsNavigate(false); }} > Next diff --git a/frontend/app/read/components/disqus.tsx b/frontend/app/read/components/disqus.tsx index 87708e0729854466d7f7017aa62bfddbd1816226..b63d9fa7938cb7d4f4ce81113b68a7304f19ecc5 100644 --- a/frontend/app/read/components/disqus.tsx +++ b/frontend/app/read/components/disqus.tsx @@ -17,15 +17,10 @@ const Disqus = ({url,identifier,title, paddingVertical=0, paddingHorizontal=0}:a const Dimensions = useWindowDimensions(); const shortname = 'comicmtl'; - - const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) - const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) - const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + if (Platform.OS === "web") { - - return ( + + +); +} diff --git a/frontend/app/recent/components/comic_component.tsx b/frontend/app/recent/components/comic_component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5a360ae45df02905efcfc62259573b1779db64d1 --- /dev/null +++ b/frontend/app/recent/components/comic_component.tsx @@ -0,0 +1,194 @@ +import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react'; +import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router'; +import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper'; +import CircularProgress from 'react-native-circular-progress-indicator'; +import { ActivityIndicator } from 'react-native-paper'; +import { FlashList } from "@shopify/flash-list"; + + +import uuid from 'react-native-uuid'; +import Toast from 'react-native-toast-message'; +import { View, AnimatePresence } from 'moti'; +import * as Clipboard from 'expo-clipboard'; +import * as FileSystem from 'expo-file-system'; +import NetInfo from "@react-native-community/netinfo"; +import { Marquee } from '@animatereactnative/marquee'; +import { Slider } from '@rneui/themed-edge'; + +import { __styles } from '../stylesheet/styles'; +import Storage from '@/constants/module/storages/storage'; +import ChapterStorage from '@/constants/module/storages/chapter_storage'; +import CoverStorage from '@/constants/module/storages/cover_storage'; + +import Image from '@/components/Image'; +import {CONTEXT} from '@/constants/module/context'; +import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager"; +import Theme from '@/constants/theme'; +import ComicStorage from '@/constants/module/storages/comic_storage'; + +const ComicComponent = ({index, item, SELECTED_BOOKMARK, SET_SELECTED_BOOKMARK}:any) => { + const Dimensions = useWindowDimensions(); + const controller = new AbortController(); + const signal = controller.signal; + + const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) + const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) + const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + const {widgetContext, setWidgetContext}:any = useContext(CONTEXT) + const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) + + const [styles, setStyles]:any = useState(null) + const [isLoading, setIsLoading] = useState(true) + + const cover:any = useRef("") + + useFocusEffect(useCallback(() => { + (async ()=>{ + setIsLoading(true) + setStyles(__styles(themeTypeContext,Dimensions)) + const stored_bookmark = await Storage.get("bookmark") || [] + console.log(stored_bookmark) + cover.current = await CoverStorage.get(`${item.source}-${item.id}`) || "" + setIsLoading(false) + })() + + return () => { + cover.current = "" + controller.abort(); + }; + },[])) + + return (<>{styles && !isLoading && <> + <>{index === 0 + ? = 700 ? "row" : "column", + justifyContent:"space-around", + alignItems:"center", + gap:12, + }} + > + + {router.navigate(`/view/${item.source}/${item.id}/?mode=local`)}} + style={{...styles.item_box, marginHorizontal:12,}} + > + <> + {console.log("load image error",error)}} source={cover.current} + style={styles.item_cover} + contentFit="cover" transition={1000} + onLoadEnd={()=>{cover.current = ""}} + /> + + + = 700 ? "100%" : "auto", + paddingVertical:12, + alignItems:"center", + justifyContent:"space-around", + gap:12, + }} + > + {item.info.title} + + { + router.replace(`/read/${item.source}/${item.id}/${item.history.idx}/`) + }} + > + + + Continue + + + {item.history.title} + + + + + + + + + + + : {router.navigate(`/view/${item.source}/${item.id}/?mode=local`)}} + style={styles.item_box} + > + <> + {console.log("load image error",error)}} source={cover.current} + style={styles.item_cover} + contentFit="cover" transition={1000} + onLoadEnd={()=>{cover.current = ""}} + /> + + {item.info.title} + + + } + }) + + +} + +export default ComicComponent; + diff --git a/frontend/app/recent/components/widgets/bookmark.tsx b/frontend/app/recent/components/widgets/bookmark.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6dfee6f2c98b053368ed88dac7a8ccc0357d1da --- /dev/null +++ b/frontend/app/recent/components/widgets/bookmark.tsx @@ -0,0 +1,904 @@ + +import React, { useEffect, useState, useCallback, useContext, useRef, Fragment, memo } from 'react'; +import { Platform, useWindowDimensions, ScrollView } from 'react-native'; + +import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper'; +import { View, AnimatePresence } from 'moti'; +import Toast from 'react-native-toast-message'; +import * as FileSystem from 'expo-file-system'; +import axios from 'axios'; + + +import Theme from '@/constants/theme'; +import Dropdown from '@/components/dropdown'; +import { CONTEXT } from '@/constants/module/context'; +import Storage from '@/constants/module/storages/storage'; +import ComicStorage from '@/constants/module/storages/comic_storage'; +import ImageCacheStorage from '@/constants/module/storages/image_cache_storage'; +import ChapterStorage from '@/constants/module/storages/chapter_storage'; +import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage'; + +interface BookmarkWidgetProps { + setIsLoading: any; + onRefresh: any; +} + +const BookmarkWidget: React.FC = ({ + setIsLoading, + onRefresh, +}) => { + const Dimensions = useWindowDimensions(); + + const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) + const {widgetContext, setWidgetContext}:any = useContext(CONTEXT) + const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) + const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + + const [BOOKMARK_DATA, SET_BOOKMARK_DATA]: any = useState([]) + const [MIGRATE_BOOKMARK_DATA, SET_MIGRATE_BOOKMARK_DATA]:any = useState([]) + + + const [showMenuOption, setShowMenuOption]:any = useState({state:false,positions:[0,0,0,0],id:""}) + const [searchTag, setSearchTag]:any = useState("") + + const [migrateTag,setMigrateTag]:any = useState("") + + const [manageBookmark, setManageBookmark]:any = useState({edit:"",delete:""}) + const [createTag, setCreateTag]:any = useState({state:false,title:""}) + const [removeTag, setRemoveTag]:any = useState({state:false, removing: false}) + + + const controller = new AbortController(); + const signal = controller.signal; + + const RenderTag = useCallback(({item}:any) =>{ + const [editTag, setEditTag]:any = useState(item.value) + useEffect(()=>{ + },[manageBookmark]) + + return (<> + {item.value.includes(searchTag) && + ( + + <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value && + ( + {item.label} + + + { + if (manageBookmark.edit){ + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + } + + const x = event.nativeEvent.pageX + const y = event.nativeEvent.pageY + + setShowMenuOption({ + ...showMenuOption, + state: showMenuOption.id === item.value ? false : true, + positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0], + id:showMenuOption.id === item.value ? "" : item.value, + }) + + + + }} + > + + + + + ) + } + <>{manageBookmark.edit === item.value && + ( + + } + style={{ + backgroundColor:Theme[themeTypeContext].background_color, + borderColor:Theme[themeTypeContext].border_color, + + }} + outlineColor={Theme[themeTypeContext].text_input_border_color} + value={editTag} + onChangeText={(text)=>{ + setEditTag(text) + }} + /> + + + { + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + setShowMenuOption({...showMenuOption,state:false,id:""}) + }} + > + + + + { + const stored_bookmark = await Storage.get("bookmark"); + + const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit); + + if (index !== -1){ + stored_bookmark[index] = editTag; + await Storage.store("bookmark", stored_bookmark) + + const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit) + for (const item of stored_comics){ + await ComicStorage.replaceTag(item.source, item.id, editTag) + } + + + + + const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit); + if (index_2 !== -1){ + BOOKMARK_DATA[index_2].label = editTag + BOOKMARK_DATA[index_2].value = editTag + } + SET_BOOKMARK_DATA(BOOKMARK_DATA) + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + + onRefresh(); + } + setShowMenuOption({...showMenuOption,state:false,id:""}) + }} + > + + + + + + + + ) + + } + + + + + + + ) + } + ) + } + ,[Theme,themeTypeContext,manageBookmark,searchTag,removeTag,createTag,showMenuOption,MIGRATE_BOOKMARK_DATA,BOOKMARK_DATA]) + + + const load_bookmark = async ()=>{ + const stored_bookmark_data = await Storage.get("bookmark") || [] + if (stored_bookmark_data.length) { + const bookmark_data:Array = [] + for (const item of stored_bookmark_data) { + bookmark_data.push({ + label:item, + value:item, + }) + } + + SET_BOOKMARK_DATA(bookmark_data.sort()) + }else SET_BOOKMARK_DATA([]) + } + + useEffect(()=>{ + SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA]) + },[BOOKMARK_DATA]) + + useEffect(()=>{ + load_bookmark() + return () => controller.abort(); + },[]) + + return (<>{BOOKMARK_DATA !== null && <> + + + + <>{!createTag.state && !removeTag.state && <> + + <>{BOOKMARK_DATA.length + ? <> + + { + setSearchTag(event.nativeEvent.text) + }} + /> + + + + <>{BOOKMARK_DATA.map((item:any) => + ( + + + + + ) + )} + + + + : <> + No tag found + + + } + + + + + + + } + + <>{createTag.state && + <> + + } + style={{ + + backgroundColor:Theme[themeTypeContext].background_color, + borderColor:Theme[themeTypeContext].border_color, + + }} + outlineColor={Theme[themeTypeContext].text_input_border_color} + value={createTag.title} + onChange={(event)=>{ + setCreateTag({...createTag,title:event.nativeEvent.text}) + }} + /> + + + + + + + } + + + <>{showMenuOption.state && + + { + setManageBookmark({...manageBookmark,edit:showMenuOption.id}) + setShowMenuOption({...showMenuOption,state:false}) + }} + > + + + + Edit + + + + + { + setManageBookmark({...manageBookmark,edit:"",delete:showMenuOption.id}) + setShowMenuOption({...showMenuOption,state:false}) + }} + > + + + + Delete + + + + + + + } + <>{manageBookmark.delete && ( + + + + + + Delete Tag: "{manageBookmark.delete}" + + + + item.value !== manageBookmark.delete)} + value={migrateTag} + onChange={(async (item:any) => { + setMigrateTag(item.value) + })} + /> + + <>{!migrateTag && ( + Setting migration to None will remove all comics and chapters for this bookmark tag. + )} + + <>{migrateTag + + ? { + const stored_bookmark = await Storage.get("bookmark") + const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete) + if (index === -1) return + + const stored_comics = await ComicStorage.getByTag(manageBookmark.delete) + for (const comic of stored_comics) { + const source = comic.source; + const comic_id = comic.id + await ComicStorage.replaceTag(source,comic_id,migrateTag) + + } + + stored_bookmark.splice(index, 1); + await Storage.store("bookmark",stored_bookmark); + + + + + const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete); + if (index_2 !== -1){ + BOOKMARK_DATA.splice(index_2, 1); + } + SET_BOOKMARK_DATA(BOOKMARK_DATA) + setManageBookmark({...manageBookmark,edit:"",delete:""}) + setShowMenuOption({...showMenuOption,state:false,id:""}) + setMigrateTag("") + + onRefresh(); + }} + > + + Migrate + + + : { + const stored_bookmark = await Storage.get("bookmark"); + const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete) + if (index === -1) return + await ComicStorage.removeByTag(manageBookmark.delete); + stored_bookmark.splice(index, 1); + await Storage.store("bookmark",stored_bookmark); + + const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete); + if (index_2 !== -1){ + BOOKMARK_DATA.splice(index_2, 1); + } + SET_BOOKMARK_DATA(BOOKMARK_DATA) + setManageBookmark({...manageBookmark,edit:"",delete:""}) + setShowMenuOption({...showMenuOption,state:false,id:""}) + setMigrateTag("") + onRefresh() + + }} + > + + Delete + + + } + { + setShowMenuOption({...showMenuOption,state:false,id:""}) + setManageBookmark({...manageBookmark,edit:"",delete:""}) + }} + > + + Cancel + + + + + + + + + )} + + }) +} + +export default BookmarkWidget; \ No newline at end of file diff --git a/frontend/app/recent/index.tsx b/frontend/app/recent/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..541077ad8b2c1a83fbec46e0392c5b7e4f0f8485 --- /dev/null +++ b/frontend/app/recent/index.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState, useCallback, useContext, useRef, useMemo } from 'react'; +import { Link, router, useLocalSearchParams, useFocusEffect } from 'expo-router'; +import { Image as RNImage, StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl, Platform, FlatList, TouchableOpacity } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple } from 'react-native-paper'; +import CircularProgress from 'react-native-circular-progress-indicator'; +import { ActivityIndicator } from 'react-native-paper'; +import { FlashList } from "@shopify/flash-list"; + + +import uuid from 'react-native-uuid'; +import Toast from 'react-native-toast-message'; +import { View, AnimatePresence } from 'moti'; +import * as Clipboard from 'expo-clipboard'; +import * as FileSystem from 'expo-file-system'; +import NetInfo from "@react-native-community/netinfo"; +import { Marquee } from '@animatereactnative/marquee'; +import { Slider } from '@rneui/themed-edge'; + + +import ComicComponent from './components/comic_component'; +import BookmarkWidget from './components/widgets/bookmark'; + +import { __styles } from './stylesheet/styles'; +import Storage from '@/constants/module/storages/storage'; +import ChapterStorage from '@/constants/module/storages/chapter_storage'; +import Image from '@/components/Image'; +import {CONTEXT} from '@/constants/module/context'; +import {blobToBase64, base64ToBlob, getImageLayout} from "@/constants/module/file_manager"; +import Theme from '@/constants/theme'; +import ComicStorage from '@/constants/module/storages/comic_storage'; + +const Index = ({}:any) => { + const Dimensions = useWindowDimensions(); + const controller = new AbortController(); + const signal = controller.signal; + + const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) + const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) + const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT) + const {widgetContext, setWidgetContext}:any = useContext(CONTEXT) + const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT) + + const [styles, setStyles]:any = useState("") + const [isLoading, setIsLoading] = useState(true) + const [onRefresh, setOnRefresh] = useState(false) + + + const [COMIC_DATA, SET_COMIC_DATA] = useState([]) + + + useFocusEffect(useCallback(() => { + (async ()=>{ + setIsLoading(true) + const stored_recent = await Storage.get("RECENT") || [] + stored_recent.sort((a:any,b:any) => b.timestamp - a.timestamp) + const stored_comic = [] + for (const item of stored_recent) { + const comic = await ComicStorage.getByID(item.source,item.comic_id) + if (comic) stored_comic.push(comic) + } + SET_COMIC_DATA(stored_comic) + setIsLoading(false) + })() + },[onRefresh])) + + + + const RenderComicComponent = useCallback(({item,index}:any) => { + console.log(item,index) + return + },[]) + + useFocusEffect(useCallback(() => { + setIsLoading(true) + setShowMenuContext(true) + setStyles(__styles(themeTypeContext,Dimensions)) + + return () => { + controller.abort(); + }; + },[])) + + return (<>{styles && ! isLoading + ? <> + + + Recent + + + <>{COMIC_DATA.length + ? <>{COMIC_DATA.map((item:any,index:number) => ( + + )) + } + : + <> + + There no recent read. + + + } + + + + + + : + + + }) + + +} + +export default Index; + diff --git a/frontend/app/recent/stylesheet/styles.tsx b/frontend/app/recent/stylesheet/styles.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6e8d649a632aaa2441d3466e7f6e6e02823d5672 --- /dev/null +++ b/frontend/app/recent/stylesheet/styles.tsx @@ -0,0 +1,63 @@ +import { StyleSheet } from "react-native"; +import Theme from "@/constants/theme"; + +export const __styles:any = (theme_type:string,Dimensions:any) => { + return StyleSheet.create({ + screen_container: { + display: "flex", + width: "100%", + height: "100%", + backgroundColor: Theme[theme_type].background_color, + }, + header_container: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 15, + paddingVertical:10, + backgroundColor: Theme[theme_type].background_color, + borderBottomWidth: 0.5, + borderColor: Theme[theme_type].border_color, + }, + header_text:{ + fontFamily: "roboto-medium", + fontSize: ((Dimensions.width+Dimensions.height)/2)*0.04, + color: Theme[theme_type].text_color, + + }, + + item_box:{ + display:"flex", + flexDirection:"column", + alignItems:"center", + gap:15, + height:"auto", + width:Math.max(((Dimensions.width+Dimensions.height)/2)*0.225,100), + borderRadius:8, + + }, + item_cover:{ + width:"100%", + height:Math.max(((Dimensions.width+Dimensions.height)/2)*0.325,125), + borderRadius:8, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.22, + shadowRadius: 2.22, + + elevation: 3, + }, + item_title:{ + color: Theme[theme_type].text_color, + fontFamily: "roboto-medium", + fontSize: ((Dimensions.width+Dimensions.height)/2)*0.025, + width:"100%", + height:"auto", + textAlign:"center", + flexShrink:1, + } + })} \ No newline at end of file diff --git a/frontend/app/view/[source]/[comic_id].tsx b/frontend/app/view/[source]/[comic_id].tsx index 40e42aa7143072b615ef05ec4786f18b0eb405b5..9df788f1a6cd68e840c00647964bf35d267f961a 100644 --- a/frontend/app/view/[source]/[comic_id].tsx +++ b/frontend/app/view/[source]/[comic_id].tsx @@ -12,6 +12,8 @@ import Toast from 'react-native-toast-message'; import { View, AnimatePresence } from 'moti'; import * as Clipboard from 'expo-clipboard'; import NetInfo from "@react-native-community/netinfo"; +import _ from 'lodash' + import Theme from '@/constants/theme'; @@ -20,6 +22,8 @@ import Storage from '@/constants/module/storages/storage'; import ImageCacheStorage from '@/constants/module/storages/image_cache_storage'; import ChapterStorage from '@/constants/module/storages/chapter_storage'; import ComicStorage from '@/constants/module/storages/comic_storage'; +import CoverStorage from '@/constants/module/storages/cover_storage'; + import { CONTEXT } from '@/constants/module/context'; import Dropdown from '@/components/dropdown'; import PageNavigationWidget from '../componenets/widgets/page_navigation'; @@ -39,6 +43,8 @@ import { createSocket, setupSocketNetworkListener } from '../modules/socket'; const Index = ({}:any) => { const SOURCE = useLocalSearchParams().source; const ID = useLocalSearchParams().comic_id; + const MODE = useLocalSearchParams().mode; + console.log(MODE) const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT) @@ -61,10 +67,9 @@ const Index = ({}:any) => { const [CONTENT, SET_CONTENT]:any = useState({}) - const [chapterRequested, setChapterRequested]:any = useState({}) - const [chapterToDownload, setChapterToDownload]:any = useState({}) - const [downloadProgress, setDownloadProgress]:any = useState(0) - const [chapterQueue, setChapterQueue]:any = useState({}) + + + const [isLoading, setIsLoading]:any = useState(true); const [feedBack, setFeedBack]:any = useState(""); const [showOption, setShowOption]:any = useState({type:null}) @@ -77,6 +82,11 @@ const Index = ({}:any) => { const socketNetWorkListener:any = useRef(null) const socket:any = useRef(null) + + const download_progress:any = useRef({}) + const chapter_queue:any = useRef({}) + const chapter_requested:any = useRef({}) + const chapter_to_download:any = useRef({}) const controller = new AbortController(); const signal = controller.signal; @@ -87,30 +97,40 @@ const Index = ({}:any) => { },[CONTENT]) + const RenderChapter = useCallback(({chapter}:any) => { + return + },[page,sort]) + // Worker for downloading chapter const download_chapter_interval:any = useRef(null) const isDownloading:any = useRef(false) - useEffect(() => { + useFocusEffect(useCallback(() => { clearInterval(download_chapter_interval.current) - download_chapter_interval.current = setInterval(() => { - if (!isDownloading.current && Object.keys(chapterToDownload).length){ + if (!isDownloading.current && Object.keys(chapter_to_download.current).length){ isDownloading.current = true - console.log(isDownloading.current,chapterToDownload) - console.log("Downloading HERE") download_chapter( setShowCloudflareTurnstileContext, isDownloading, SOURCE, ID, - chapterRequested, setChapterRequested, - chapterToDownload, setChapterToDownload, - downloadProgress, setDownloadProgress, - signal, + chapter_requested, chapter_to_download, download_progress, signal, ) } },1000) return () => clearInterval(download_chapter_interval.current) - },[chapterToDownload]) + },[])) // Setting up socket listener useFocusEffect(useCallback(() => { @@ -130,10 +150,9 @@ const Index = ({}:any) => { if (!stored_comic) return const event = result.event if (event.type === "chapter_queue_info"){ - console.log(event.chapter_queue) - setChapterQueue(event.chapter_queue) + chapter_queue.current = event.chapter_queue; }else if (event.type === "chapter_ready_to_download"){ - get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID) + get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID) } } } @@ -166,11 +185,11 @@ const Index = ({}:any) => { },[])) - const Load_Offline = async () => { + const Load_Local = async () => { Toast.show({ type: 'info', - text1: '🌐 No internet connection available.', - text2: `Switching to offline mode.`, + text1: '💾 Local mode', + text2: `Press refresh button to fetch new updates.`, position: "bottom", visibilityTime: 6000, @@ -191,6 +210,7 @@ const Index = ({}:any) => { if (stored_comic) { const DATA:any = {} DATA["id"] = ID + DATA["cover"] = await CoverStorage.get(`${SOURCE}-${ID}`) for (const [key, value] of Object.entries(stored_comic.info)) { DATA[key] = value @@ -244,18 +264,18 @@ const Index = ({}:any) => { const stored_comic = await ComicStorage.getByID(SOURCE,ID) if (stored_comic) { - await get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID) - setBookmarked({state:true,tag:stored_comic.tag}) setHistory(stored_comic.history) } else setBookmarked({state:false,tag:""}) const net_info = await NetInfo.fetch() - if (net_info.isConnected){ + if (net_info.isConnected && MODE !== "local"){ + if (stored_comic) await get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID) get(setShowCloudflareTurnstileContext, setIsLoading, signal, __translate, setFeedBack, SOURCE, ID, SET_CONTENT) }else{ - Load_Offline() + if (net_info.isConnected && stored_comic) await get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID) + Load_Local() } })() @@ -280,11 +300,10 @@ const Index = ({}:any) => { if (net_info.isConnected){ get(setShowCloudflareTurnstileContext, setIsLoading, signal, translate, setFeedBack, SOURCE, ID, SET_CONTENT) if (stored_comic) { - setHistory(stored_comic.history) - get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID) + get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID) } }else{ - Load_Offline() + Load_Local() } } @@ -312,7 +331,7 @@ const Index = ({}:any) => { onPress={()=>{ if (router.canGoBack()) router.back() - else router.replace("/explore") + else router.replace("/bookmark") }} > @@ -354,7 +373,7 @@ const Index = ({}:any) => { onRefresh() }} > - + { Toast.show({ type: 'info', text1: '📋 Copied to your clipboard.', - text2: `${apiBaseContext}/view/${SOURCE}/${ID}/`, + text2: `https://comicmtl.netlify.app/view/${SOURCE}/${ID}/`, position: "bottom", visibilityTime: 3000, @@ -583,6 +602,7 @@ const Index = ({}:any) => { onPress={()=>{ setWidgetContext({state:true,component: { }} onPress={()=>{ - router.push(`/read/${SOURCE}/${ID}/${history.idx}/`) + router.replace(`/read/${SOURCE}/${ID}/${history.idx}/`) }} > { : { - - if (bookmarked.state){ - let chapter - if (sort === "descending"){ - chapter = CONTENT?.chapters[CONTENT.chapters.length - 1] - }else{ - chapter = CONTENT?.chapters[0] - } - const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id) - console.log(stored_chapter) - if (stored_chapter.data_state === "completed"){ - await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title}) - router.push(`/read/${SOURCE}/${ID}/${stored_chapter.idx}/`) + rippleColor={Theme[themeTypeContext].ripple_color_outlined} + style={{ + width:Dimensions.width*0.60, + display:"flex", + flexDirection:"column", + justifyContent:"center", + alignSelf:"center", + padding:8, + paddingVertical:12, + borderRadius:Dimensions.width*0.60/2, + backgroundColor:Theme[themeTypeContext].border_color, + + shadowColor: Theme[themeTypeContext].shadow_color, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + + }} + onPress={async ()=>{ + + if (bookmarked.state){ + let chapter + if (sort === "descending"){ + chapter = CONTENT?.chapters[CONTENT.chapters.length - 1] + }else{ + chapter = CONTENT?.chapters[0] + } + const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id) + if (stored_chapter.data_state === "completed"){ + await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title}) + router.replace(`/read/${SOURCE}/${ID}/${stored_chapter.idx}/`) + + }else{ + Toast.show({ + type: 'error', + text1: 'Chapter not download yet.', + text2: "Press the button next to chapter title to download.", + + position: "bottom", + visibilityTime: 4000, + text1Style:{ + fontFamily:"roboto-bold", + fontSize:((Dimensions.width+Dimensions.height)/2)*0.025 + }, + text2Style:{ + fontFamily:"roboto-medium", + fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185, + + }, + }); + } }else{ Toast.show({ type: 'error', - text1: 'Chapter not download yet.', - text2: "Press the button next to chapter title to download.", + text1: '🔖 Bookmark required.', + text2: `Add this comic to your bookmark to start reading.`, position: "bottom", visibilityTime: 4000, @@ -730,30 +769,11 @@ const Index = ({}:any) => { }, }); - } - }else{ - Toast.show({ - type: 'error', - text1: '🔖 Bookmark required.', - text2: `Add this comic to your bookmark to start reading.`, - - position: "bottom", - visibilityTime: 4000, - text1Style:{ - fontFamily:"roboto-bold", - fontSize:((Dimensions.width+Dimensions.height)/2)*0.025 - }, - text2Style:{ - fontFamily:"roboto-medium", - fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185, - - }, - }); - - } - }} - > + + } + }} + > { >Synopsis: - + > + {showMoreSynopsis ? "Show Less" : "Show More"} + @@ -821,6 +853,7 @@ const Index = ({}:any) => { borderColor: Theme[themeTypeContext].border_color, borderBottomWidth:showMoreSynopsis ? 0 : 5, borderRadius:8, + }} numberOfLines={showMoreSynopsis ? 0 : 2} ellipsizeMode='tail' @@ -870,24 +903,7 @@ const Index = ({}:any) => { <>{CONTENT.chapters.length ? <>{CONTENT.chapters.slice((page-1)*MAX_OFFSET,((page-1)*MAX_OFFSET)+MAX_OFFSET).map((chapter:any,index:number) => - + )} : { const {showMenuContext, setShowMenuContext}:any = useContext(CONTEXT) @@ -57,9 +54,87 @@ const ChapterComponent = ({ const [styles, setStyles]:any = useState("") const [is_saved, set_is_saved] = useState(false) + const [isLoading, setIsLoading] = useState(true); const [is_net_connected, set_is_net_connected]:any = useState(false) + + const [chapterQueue, setChapterQueue]:any = useState({}) + const [chapterRequested, setChapterRequested]:any = useState({}) + const [chapterToDownload, setChapterToDownload]:any = useState({}) + const [downloadProgress, setDownloadProgress]:any = useState({[chapter.id]:{progress:0,total:100}}) + + + const chapter_status_interval = useRef(null) + const is_running = useRef(false) + + useEffect(()=>{ + // console.log("CTD", chapterToDownload) + },[chapterToDownload]) + + useEffect(()=>{ + // console.log("CR", chapterRequested) + },[chapterRequested]) + + useFocusEffect(useCallback(() => { + clearInterval(chapter_status_interval.current) + chapter_status_interval.current = setInterval(() => { + if (is_running.current) return + is_running.current = true + + // current -> Checking for chapter requested + if (chapter_requested.current.hasOwnProperty(chapter.id) && !chapterRequested.hasOwnProperty(chapter.id)) { + setChapterRequested({[chapter.id]:chapter_requested.current[chapter.id]}) + }else if ( + chapter_requested.current.hasOwnProperty(chapter.id) + && chapter_requested.current[chapter.id]?.state !== chapterRequested[chapter.id]?.state + ) { + setChapterRequested({[chapter.id]:chapter_requested.current[chapter.id]}) + }else if (!chapter_requested.current.hasOwnProperty(chapter.id) && chapterRequested.hasOwnProperty(chapter.id)){ + setChapterRequested({}) + } + + // current -> Checking for chapter to download + if (chapter_to_download.current.hasOwnProperty(chapter.id) && !chapterToDownload.hasOwnProperty(chapter.id)) { + setChapterToDownload({[chapter.id]:chapter_to_download.current[chapter.id]}) + }else if ( + chapter_to_download.current.hasOwnProperty(chapter.id) + && chapter_to_download.current[chapter.id]?.state !== chapterToDownload[chapter.id]?.state + ) { + setChapterToDownload({[chapter.id]:chapter_to_download.current[chapter.id]}) + }else if (!chapter_to_download.current.hasOwnProperty(chapter.id) && chapterToDownload.hasOwnProperty(chapter.id)){ + setChapterToDownload({}) + } + + // current -> Check download progress + if (download_progress.current.hasOwnProperty(chapter.id) && !downloadProgress.hasOwnProperty(chapter.id)) { + setDownloadProgress({[chapter.id]:download_progress.current[chapter.id]}) + }else if ( + download_progress.current.hasOwnProperty(chapter.id) + && ( + download_progress.current[chapter.id]?.progress !== downloadProgress[chapter.id]?.progress + || download_progress.current[chapter.id]?.total !== downloadProgress[chapter.id]?.total + ) + ) { + setDownloadProgress({[chapter.id]:download_progress.current[chapter.id]}) + }else if (!download_progress.current.hasOwnProperty(chapter.id) && downloadProgress.hasOwnProperty(chapter.id)){ + setDownloadProgress({}) + } + + // current -> Check chapter Queue + if (chapter_queue.current?.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)) { + setChapterQueue(chapter_queue.current) + }else if (chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`)){ + setChapterQueue({}) + } + + is_running.current = false + },3000) + return () => clearInterval(chapter_status_interval.current) + },[chapterQueue,chapterRequested,chapterToDownload, downloadProgress])) + + useEffect(() => {(async () => { + setIsLoading(true) const net_info = await NetInfo.fetch() set_is_net_connected(net_info.isConnected) @@ -67,27 +142,26 @@ const ChapterComponent = ({ const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id) if (stored_chapter?.data_state === "completed") set_is_saved(true) else set_is_saved(false) + setIsLoading(false) })()}, []) useEffect(()=>{(async () => { + setIsLoading(true) const stored_chapter = await ChapterStorage.get(`${SOURCE}-${ID}`,chapter.id) if (stored_chapter?.data_state === "completed") set_is_saved(true) else set_is_saved(false) + setIsLoading(false) })()},[page,sort]) const Request_Download = async (CHAPTER:any) => { - const stored_comic = await ComicStorage.getByID(SOURCE,ID) if (stored_comic) { setWidgetContext({state:true,component: get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID)} + chapter_requested={chapter_requested} + get_requested_info={() => get_requested_info(setShowCloudflareTurnstileContext, chapter_requested, chapter_to_download, signal, SOURCE, ID)} />}) } else{ @@ -134,7 +208,8 @@ const ChapterComponent = ({ if (stored_chapter?.data_state === "completed") { const stored_comic = await ComicStorage.getByID(SOURCE,ID) 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}) - router.push(`/read/${SOURCE}/${ID}/${chapter.idx}/`) + + router.navigate(`/read/${SOURCE}/${ID}/${chapter.idx}/`) }else{ Toast.show({ type: 'error', @@ -158,142 +233,143 @@ const ChapterComponent = ({ > {chapter.title} - {is_net_connected || is_saved - ? <>{is_saved - ? - : <> - <>{chapterRequested[chapter.id]?.state === "queue" && !chapterQueue?.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) - && - } - + {isLoading + ? () + + : <>{is_net_connected || is_saved + ? <>{is_saved + ? + : <> + <>{chapterRequested[chapter.id]?.state === "queue" && !chapterQueue?.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) + && + } + - <>{chapterRequested[chapter.id]?.state === "unkown" && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) - && { - Toast.show({ - type: 'error', - text1: '❓Request not found in server.', - text2: "You request this chapter but the server doesn't have this in queue.\nTry request again.", - - position: "bottom", - visibilityTime: 12000, - text1Style:{ - fontFamily:"roboto-bold", - fontSize:((Dimensions.width+Dimensions.height)/2)*0.025 - }, - text2Style:{ - fontFamily:"roboto-medium", - fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185, - - }, - }); - Request_Download(chapter) - }} - > - - - - } - - <>{chapterRequested[chapter.id]?.state === "ready" - && <>{chapterToDownload[chapter.id]?.state === "downloading" - ? {chapterRequested[chapter.id]?.state === "unkown" && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) + && { + Toast.show({ + type: 'error', + text1: '❓Request not found in server.', + text2: "You request this chapter but the server doesn't have this in queue.\nTry request again.", + + position: "bottom", + visibilityTime: 12000, + text1Style:{ + fontFamily:"roboto-bold", + fontSize:((Dimensions.width+Dimensions.height)/2)*0.025 + }, + text2Style:{ + fontFamily:"roboto-medium", + fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185, + + }, + }); + Request_Download(chapter) }} - onAnimationComplete={async ()=>{ - if (downloadProgress[chapter.id].progress !== downloadProgress[chapter.id].total) return + > + + + + } + + <>{chapterRequested[chapter.id]?.state === "ready" + && <>{chapterToDownload[chapter.id]?.state === "downloading" + ? item.chapter_id !== chapter.id); - await ComicStorage.updateChapterQueue(SOURCE,ID,new_chapter_requested) + showProgressValue={false} + title={"📥"} + titleStyle={{ + pointerEvents:"none", + color:Theme[themeTypeContext].text_color, + fontSize:((Dimensions.width+Dimensions.height)/2)*0.025, + fontFamily:"roboto-medium", + textAlign:"center", + }} + onAnimationComplete={async ()=>{ + if (downloadProgress[chapter.id].progress !== downloadProgress[chapter.id].total) return + + const stored_chapter_requested = (await ComicStorage.getByID(SOURCE,ID)).chapter_requested + const new_chapter_requested = stored_chapter_requested.filter((item:any) => item.chapter_id !== chapter.id); + await ComicStorage.updateChapterQueue(SOURCE,ID,new_chapter_requested) - delete chapterRequested[chapter.id] - setChapterRequested(chapterRequested) + delete chapter_requested.current[chapter.id] + setChapterRequested({}) - const chapter_to_download = chapterToDownload - delete chapter_to_download[chapter.id] - setChapterToDownload(chapter_to_download) + delete chapter_to_download.current[chapter.id] + setChapterToDownload({}) - const download_progress = downloadProgress - delete download_progress[chapter.id] - setDownloadProgress(download_progress) + delete download_progress[chapter.id] + setDownloadProgress({}) - set_is_saved(true) - isDownloading.current = false - console.log("DONE!",downloadProgress[chapter.id]) - }} - /> - : + set_is_saved(true) + isDownloading.current = false + console.log("DONE DOWNLOADING!") + }} + /> + : + } } - } - - <>{chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) && !(chapterRequested[chapter.id]?.state === "ready") - && <>{chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`] - ? {chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) && !(chapterRequested[chapter.id]?.state === "ready") + && <>{chapterQueue.queue[`${SOURCE}-${ID}-${chapter.idx}`] + ? { + get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, chapter_to_download, signal, SOURCE, ID) + }} + /> + : + } + } + + <>{!chapterRequested.hasOwnProperty(chapter.id) && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) + ? { - console.log("HAHA",chapterQueue) - get_requested_info(setShowCloudflareTurnstileContext, setChapterRequested, setChapterToDownload, signal, SOURCE, ID) + + onPress={()=>{ + Request_Download(chapter) }} - /> - : + > + + + + :<> } - } - - <>{!chapterRequested.hasOwnProperty(chapter.id) && !chapterQueue.queue?.hasOwnProperty(`${SOURCE}-${ID}-${chapter.idx}`) - ? { - Request_Download(chapter) - }} - > - - - - :<> - } - + + } + : } - : } } } diff --git a/frontend/app/view/componenets/widgets/bookmark.tsx b/frontend/app/view/componenets/widgets/bookmark.tsx index 44dc1546e40fde125825455fe887f13634650981..2a4f2f1235a81c87f3fe0cc616db60b3498535a9 100644 --- a/frontend/app/view/componenets/widgets/bookmark.tsx +++ b/frontend/app/view/componenets/widgets/bookmark.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState, useCallback, useContext, useRef, Fragment } from 'react'; +import React, { useEffect, useState, useCallback, useContext, useRef, Fragment, memo } from 'react'; import { Platform, useWindowDimensions, ScrollView } from 'react-native'; 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'; import ComicStorage from '@/constants/module/storages/comic_storage'; import ImageCacheStorage from '@/constants/module/storages/image_cache_storage'; import ChapterStorage from '@/constants/module/storages/chapter_storage'; - +import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage'; interface BookmarkWidgetProps { + setIsLoading: any; onRefresh: any; SOURCE: string | string[]; ID: string | string[]; @@ -27,10 +28,11 @@ interface BookmarkWidgetProps { } const BookmarkWidget: React.FC = ({ + setIsLoading, onRefresh, SOURCE, ID, - CONTENT + CONTENT, }) => { const Dimensions = useWindowDimensions(); @@ -58,206 +60,207 @@ const BookmarkWidget: React.FC = ({ const controller = new AbortController(); const signal = controller.signal; - const RenderTag = ({item}:any) =>{ - const [editTag, setEditTag]:any = useState(item.value) - return (<> - {item.value.includes(searchTag) && - ( - - <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value && - ( - {item.label} - { + const [editTag, setEditTag]:any = useState(item.value) + useEffect(()=>{ + },[manageBookmark]) + + return (<> + {item.value.includes(searchTag) && + ( + + <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value && + ( - - { - if (manageBookmark.edit){ - setManageBookmark({...manageBookmark,edit:""}) - setEditTag("") - } - - - const x = event.nativeEvent.pageX - const y = event.nativeEvent.pageY - - setShowMenuOption({ - ...showMenuOption, - state: showMenuOption.id === item.value ? false : true, - positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0], - id:showMenuOption.id === item.value ? "" : item.value, - }) - - - + color:"white", + fontFamily:"roboto-medium", + fontSize:(Dimensions.width+Dimensions.height)/2*0.025 }} - > - - - - - ) - } - <>{manageBookmark.edit && - ( - - } - style={{ - width:"100%", - height:"100%", - backgroundColor:Theme[themeTypeContext].background_color, - borderColor:Theme[themeTypeContext].border_color, - - }} - outlineColor={Theme[themeTypeContext].text_input_border_color} - value={editTag} - onChange={(event)=>{ - setEditTag(event.nativeEvent.text) - }} - /> - + >{item.label} + { - setManageBookmark({...manageBookmark,edit:""}) - setEditTag("") - setShowMenuOption({...showMenuOption,state:false,id:""}) + onPress={(event)=>{ + if (manageBookmark.edit){ + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + } + + const x = event.nativeEvent.pageX + const y = event.nativeEvent.pageY + + setShowMenuOption({ + ...showMenuOption, + state: showMenuOption.id === item.value ? false : true, + positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0], + id:showMenuOption.id === item.value ? "" : item.value, + }) + + + }} > - + - + ) + } + <>{manageBookmark.edit === item.value && + ( + + } + style={{ + backgroundColor:Theme[themeTypeContext].background_color, + borderColor:Theme[themeTypeContext].border_color, + + }} + outlineColor={Theme[themeTypeContext].text_input_border_color} + value={editTag} + onChangeText={(text)=>{ + setEditTag(text) + }} + /> + + + { - const stored_bookmark = await Storage.get("bookmark"); + onPress={()=>{ + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") + setShowMenuOption({...showMenuOption,state:false,id:""}) + }} + > - const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit); - - if (index !== -1){ - stored_bookmark[index] = editTag; - await Storage.store("bookmark", stored_bookmark) + + + }); + onPress={async ()=>{ + const stored_bookmark = await Storage.get("bookmark"); + + const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit); + + if (index !== -1){ + stored_bookmark[index] = editTag; + await Storage.store("bookmark", stored_bookmark) - }else{ - - const index = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit); - if (index !== -1){ - BOOKMARK_DATA[index].label = editTag - BOOKMARK_DATA[index].value = editTag + const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit) + for (const item of stored_comics){ + await ComicStorage.replaceTag(item.source, item.id, editTag) + } + if ((manageBookmark.edit === defaultTag) && (editTag !== defaultTag)) { + onRefresh(); + setWidgetContext({state:false,component:<>}); + + }else{ + + const index = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit); + if (index !== -1){ + BOOKMARK_DATA[index].label = editTag + BOOKMARK_DATA[index].value = editTag + } + SET_BOOKMARK_DATA(BOOKMARK_DATA) + setManageBookmark({...manageBookmark,edit:""}) + setEditTag("") } - SET_BOOKMARK_DATA(BOOKMARK_DATA) - setManageBookmark({...manageBookmark,edit:""}) - setEditTag("") + } - - } - setShowMenuOption({...showMenuOption,state:false,id:""}) - }} - > + setShowMenuOption({...showMenuOption,state:false,id:""}) + }} + > + + + + + - - - - - - ) + ) - } + } + + + + - - - - - ) - } - ) - } - + ) + } + ) + } + ,[Theme,themeTypeContext,manageBookmark,searchTag,removeTag,createTag,defaultTag,bookmark,showMenuOption,MIGRATE_BOOKMARK_DATA,BOOKMARK_DATA]) const load_bookmark = async ()=>{ @@ -283,7 +286,6 @@ const BookmarkWidget: React.FC = ({ } useEffect(()=>{ - console.log(BOOKMARK_DATA) SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA]) },[BOOKMARK_DATA]) @@ -298,8 +300,8 @@ const BookmarkWidget: React.FC = ({ style={{ zIndex:10, backgroundColor:Theme[themeTypeContext].background_color, - width:Dimensions.width*0.35, - minWidth:500, + maxWidth:500, + width:"100%", borderColor:Theme[themeTypeContext].border_color, borderWidth:2, @@ -351,7 +353,7 @@ const BookmarkWidget: React.FC = ({ theme_type={themeTypeContext} Dimensions={Dimensions} - label='Add to bookmark' + label='Add to tag' data={BOOKMARK_DATA} value={bookmark} onChange={(async (item:any) => { @@ -420,10 +422,9 @@ const BookmarkWidget: React.FC = ({ const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id) if (stored_comic) await ComicStorage.replaceTag(SOURCE, CONTENT.id, bookmark) else { - const cover_result:any = await store_comic_cover(setShowCloudflareTurnstileContext,signal,SOURCE,ID,CONTENT) + await store_comic_cover(setShowCloudflareTurnstileContext,signal,SOURCE,ID,CONTENT) await ComicStorage.store(SOURCE,CONTENT.id, bookmark, { - cover:cover_result, title:CONTENT.title, author:CONTENT.author, category:CONTENT.category, @@ -457,7 +458,7 @@ const BookmarkWidget: React.FC = ({ style={{flex:1}} > = ({ }} > - <>{BOOKMARK_DATA.map((item:any) => - - - )} + <>{BOOKMARK_DATA.map((item:any) => + ( + + + + + ) + )} @@ -505,7 +510,7 @@ const BookmarkWidget: React.FC = ({ color:Theme[themeTypeContext].text_color, fontSize:(Dimensions.width+Dimensions.height)/2*0.045, fontFamily:"roboto-bold", - }}>No bookmark found + }}>No tag found } @@ -530,7 +535,7 @@ const BookmarkWidget: React.FC = ({ setCreateTag({state:true,title:""}) setShowMenuOption({...showMenuOption,state:false,id:""}) })} - >+ Create Bookmark + >+ Create @@ -809,12 +816,12 @@ const BookmarkWidget: React.FC = ({ }} > - + = ({ display:"flex", justifyContent:"center", alignItems:"center", + padding:15, }} > = ({ SOURCE, ID, CHAPTER, - chapterQueue, - setChapterQueue, - chapterRequested, - setChapterRequested, + chapter_requested, get_requested_info }) => { const Dimensions = useWindowDimensions(); @@ -102,7 +96,7 @@ const RequestChapterWidget: React.FC = ({ theme_type={themeTypeContext} Dimensions={Dimensions} - label='Colorize' + label='Colorization' data={[ { label: "Enable", @@ -122,7 +116,7 @@ const RequestChapterWidget: React.FC = ({ theme_type={themeTypeContext} Dimensions={Dimensions} - label='Translation' + label='Translation (Beta)' data={[ { label: "Enable", @@ -219,9 +213,7 @@ const RequestChapterWidget: React.FC = ({ style={{backgroundColor:"green",borderRadius:5}} onPress={(async ()=>{ setIsRequesting(true) - const new_queue:any = {} - new_queue[CHAPTER.id] = "queue" - setChapterRequested({...chapterRequested,...new_queue}) + chapter_requested.current[CHAPTER.id] = {state: "queue"} const API_BASE = await Storage.get("IN_USE_API_BASE") const stored_socket_info = await Storage.get("SOCKET_INFO") diff --git a/frontend/app/view/modules/content.tsx b/frontend/app/view/modules/content.tsx index 303a647bbb8ba88896f2f76a507ecd936fedf051..51350c5aba073348f6081a0bb516819c2d9f2aff 100644 --- a/frontend/app/view/modules/content.tsx +++ b/frontend/app/view/modules/content.tsx @@ -10,6 +10,8 @@ import ComicStorage from '@/constants/module/storages/comic_storage'; import ImageCacheStorage from '@/constants/module/storages/image_cache_storage'; import ChapterStorage from '@/constants/module/storages/chapter_storage'; import ChapterDataStorage from '@/constants/module/storages/chapter_data_storage'; +import CoverStorage from '@/constants/module/storages/cover_storage'; + import {blobToBase64} from '@/constants/module/file_manager'; import { getImageLayout } from '@/constants/module/file_manager'; @@ -56,9 +58,7 @@ export const get = async (setShowCloudflareTurnstile:any,setIsLoading:any,signal // Store in local if bookmarked. const stored_comic = await ComicStorage.getByID(source,DATA.id) if (stored_comic) { - const cover_result:any = await store_comic_cover(setShowCloudflareTurnstile,signal,source,id,DATA) await ComicStorage.updateInfo(source,DATA.id, { - cover:cover_result, title:DATA.title, author:DATA.author, category:DATA.category, @@ -66,6 +66,7 @@ export const get = async (setShowCloudflareTurnstile:any,setIsLoading:any,signal synopsis:DATA.synopsis, updated:DATA.updated, }) + await store_comic_cover(setShowCloudflareTurnstile,signal,source,id,DATA) for (const chapter of DATA.chapters) { const stored_chapter = await ChapterStorage.get(`${source}-${DATA.id}`,chapter.id) if (!stored_chapter) await ChapterStorage.add(`${source}-${DATA.id}`, chapter.idx, chapter.id, chapter.title, {}); @@ -103,21 +104,24 @@ export const store_comic_cover = async ( to: storage_dir + "cover.png", }); - return {type:"file_path",data:storage_dir + "cover.png"} + }catch (error: any) { console.log("store_comic_cover: ", error) - return { type: "error", data: null }; + } - }else return result + }else { + CoverStorage.store(`${source}-${comic_id}`,result) + // return result + } } export const get_requested_info = async ( setShowCloudflareTurnstile:any, - setChapterRequested:any, - setChapterToDownload:any, + chapter_requested:any, + chapter_to_download:any, signal:any, source:any, comic_id:any, @@ -141,22 +145,21 @@ export const get_requested_info = async ( signal:signal, }).then((response) => { const DATA = response.data - - setChapterRequested(DATA) + chapter_requested.current = DATA const new_obj:any = {} for (const [key, value] of Object.entries(DATA) as any) { - - if (value.state === "ready") { new_obj[key] = { chapter_idx: value.chapter_idx, options: value.options, + state: chapter_to_download.current[key]?.state, } } } - console.log(new_obj) - setChapterToDownload(new_obj) + + chapter_to_download.current = new_obj + }).catch((error) => { console.log(error) @@ -170,17 +173,31 @@ export const download_chapter = async ( isDownloading:any, source:string | string[], comic_id:string | string[], - chapterRequested:any, - setChapterRequested:any, - chapterToDownload:any, - setChapterToDownload:any, - downloadProgress:any, - setDownloadProgress:any, + chapter_requested:any, + chapter_to_download:any, + download_progress:any, signal:any ) => { const API_BASE = await Storage.get("IN_USE_API_BASE") + const [chapter_id, request_info]:any = Object.entries(chapter_to_download.current)[0]; + + const stored_chapter = await ChapterStorage.get(`${source}-${comic_id}`,chapter_id) + if (stored_chapter.data_state === "completed") { + + const stored_chapter_requested = (await ComicStorage.getByID(source,comic_id)).chapter_requested + const new_chapter_requested = stored_chapter_requested.filter((item:any) => item.chapter_id !== chapter_id); + await ComicStorage.updateChapterQueue(source,comic_id,new_chapter_requested) + + delete chapter_requested.current[chapter_id] + + delete chapter_to_download.current[chapter_id] + + + delete download_progress.current[chapter_id] - const [chapter_id, request_info]:any = Object.entries(chapterToDownload)[0]; + isDownloading.current = false + return + } var progress_lenth:number = 0 var total_length:number = 0 @@ -205,12 +222,20 @@ export const download_chapter = async ( total_length = (_total_length as number) + (_total_length as number)*0.25 if (progress_lenth === 0) { - setDownloadProgress({...downloadProgress, [chapter_id]:{progress:progress_lenth, total:total_length}}) - setChapterToDownload({...chapterToDownload,[chapter_id]:{...chapterToDownload[chapter_id],state:"downloading"}}) + download_progress.current = { + ...download_progress.current, + [chapter_id]:{progress:progress_lenth, + total:total_length + }} + chapter_to_download.current = { + ...chapter_to_download.current, + [chapter_id]:{...chapter_to_download.current[chapter_id], + state:"downloading" + }} progress_lenth = progressEvent.loaded; }else{ progress_lenth = progressEvent.loaded; - setDownloadProgress({...downloadProgress, [chapter_id]:{progress:progress_lenth, total:total_length}}) + download_progress.current = {...download_progress.current, [chapter_id]:{progress:progress_lenth, total:total_length}} } } }, @@ -246,7 +271,7 @@ export const download_chapter = async ( // await ChapterStorage.update(`${source}-${comic_id}`,chapter_id,{type:"file_path", value:chapter_dir + `${request_info.chapter_idx}.zip`}, "completed") } - setDownloadProgress({...downloadProgress, [chapter_id]:{progress:total_length, total:total_length}}) + download_progress.current = {...download_progress.current, [chapter_id]:{progress:total_length, total:total_length}} @@ -255,12 +280,11 @@ export const download_chapter = async ( if (error.status === 511) setShowCloudflareTurnstile(true) else { - const chapter_to_download = chapterToDownload - delete chapter_to_download[chapter_id] - setChapterToDownload(chapter_to_download) + delete chapter_requested.current[chapter_id] + delete chapter_to_download.current[chapter_id] - const chapter_requested = (await ComicStorage.getByID(source,comic_id)).chapter_requested - const new_chapter_requested = chapter_requested.filter((item:any) => item.chapter_id !== chapter_id); + const stored_chapter_requested = (await ComicStorage.getByID(source,comic_id)).chapter_requested + const new_chapter_requested = stored_chapter_requested.filter((item:any) => item.chapter_id !== chapter_id); await ComicStorage.updateChapterQueue(source,comic_id,new_chapter_requested) isDownloading.current = false diff --git a/frontend/app/view/modules/socket.tsx b/frontend/app/view/modules/socket.tsx index af770034f03d6e101485215efeb3b1672f9626e7..c475d16c79b480f21c114fb71b0bff1ee20e54d5 100644 --- a/frontend/app/view/modules/socket.tsx +++ b/frontend/app/view/modules/socket.tsx @@ -18,14 +18,14 @@ const connectWebSocket = (socketBaseContext: string, socket_id: string | number[ console.log('WebSocket closed or error. Attempting to reconnect...'); setTimeout(() => { connectWebSocket(socketBaseContext, socket_id, onOpen, onMessage).then(resolve).catch(reject); - }, 3000); // Wait 3 seconds before attempting to reconnect + }, 10000); // Wait 3 seconds before attempting to reconnect }; // _socket.onclose = handleReconnect; _socket.onerror = (error: any) => { console.error('WebSocket error:', error); _socket.close(); - handleReconnect(); + // handleReconnect(); }; }); }; diff --git a/frontend/assets/icons/tag-edit-outline.png b/frontend/assets/icons/tag-edit-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..090e7f30c6f89a88af8c86cd6cab7466b6de419d --- /dev/null +++ b/frontend/assets/icons/tag-edit-outline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3a88598dcac85e1dfdec225970561fbe0e4bd71f414c82dffcdfc211ded7d5c +size 9006 diff --git a/frontend/assets/icons/tag-hidden.png b/frontend/assets/icons/tag-hidden.png new file mode 100644 index 0000000000000000000000000000000000000000..a28e8060e53abfc775982c9e4fca0602fe1f7547 --- /dev/null +++ b/frontend/assets/icons/tag-hidden.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:583800a83496bcd5b336beddc09e14d755f6d7f499a0a805498f12975d2c0196 +size 9833 diff --git a/frontend/components/Image.tsx b/frontend/components/Image.tsx index 917d46e5a8cd3dbaad54fa7d668f27ed574c9a28..c858bfeaaa5b2a9f7dcc11e85f23160e4815939a 100644 --- a/frontend/components/Image.tsx +++ b/frontend/components/Image.tsx @@ -88,16 +88,19 @@ const Image = ({source, style, onError, contentFit, transition, onLoad, onLoadEn ? <_Image + onError={onError} source={imageData.current} style={style} contentFit={contentFit} transition={transition} onLoad={()=>{ - if (onLoad) onLoad() - imageData.current = null + if (onLoad) onLoad(); + }} + onLoadEnd={()=>{ + if (onLoad) onLoadEnd(); + imageData.current = null; }} - onLoadEnd={onLoadEnd} /> : diff --git a/frontend/constants/module/storages/chapter_data_storage.tsx b/frontend/constants/module/storages/chapter_data_storage.tsx index 8439330103c86b62e99b0f705324090e6e3d249e..0edce413660447035448fab4526c182c0dd91057 100644 --- a/frontend/constants/module/storages/chapter_data_storage.tsx +++ b/frontend/constants/module/storages/chapter_data_storage.tsx @@ -101,10 +101,10 @@ class Chapter_Data_Storage_Web { request.onsuccess = (event) => { const cursor = (event.target as IDBRequest).result; if (cursor) { - store.delete(cursor.primaryKey); - cursor.continue(); + store.delete(cursor.primaryKey); + cursor.continue(); } else { - resolve(); + resolve(); } }; diff --git a/frontend/constants/module/storages/comic_storage.tsx b/frontend/constants/module/storages/comic_storage.tsx index 6b00538b1efe0567b19de5415376d9d48e9a9989..a69fe902c9dcb1c387ecdd3ae33deb6d7f3b0e43 100644 --- a/frontend/constants/module/storages/comic_storage.tsx +++ b/frontend/constants/module/storages/comic_storage.tsx @@ -2,6 +2,7 @@ import { Platform } from "react-native"; import * as SQLite from 'expo-sqlite'; import ChapterStorage from "./chapter_storage"; import ChapterDataStorage from "./chapter_data_storage"; +import CoverStorage from "./cover_storage"; const DATABASE_NAME = 'ComicStorageDB' @@ -231,6 +232,7 @@ class Comic_Storage_Web { const deleteRequest = store.delete(id); // Delete the item deleteRequest.onsuccess = () => { + CoverStorage.remove(`${source}-${id}`) resolve(); }; @@ -264,9 +266,11 @@ class Comic_Storage_Web { const source = data.source const comic_id = data.id; - ChapterStorage.drop(`${source}-${comic_id}`), + ChapterStorage.drop(`${source}-${comic_id}`) + CoverStorage.remove(`${source}-${comic_id}`) ChapterDataStorage.removeByComicID(comic_id) + cursor.delete(); cursor.continue(); diff --git a/frontend/constants/module/storages/cover_storage.tsx b/frontend/constants/module/storages/cover_storage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5700911eed3d52f9426afa3bcbecd18b2340d31f --- /dev/null +++ b/frontend/constants/module/storages/cover_storage.tsx @@ -0,0 +1,93 @@ +import { Platform } from "react-native"; +import * as SQLite from 'expo-sqlite'; + +const DATABASE_NAME = 'CoverStorageDB' + +class CoverStorage_Web { + private static dbPromise: Promise; + + private static getDB(): Promise { + if (!this.dbPromise) { + this.dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open(DATABASE_NAME, 2); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (db.objectStoreNames.contains('dataStore')) db.deleteObjectStore('dataStore'); + db.createObjectStore('dataStore'); + }; + + request.onsuccess = () => { + resolve(request.result); + }; + + request.onerror = () => { + reject(request.error); + }; + }); + } + return this.dbPromise; + } + + static async store(id: string, value: any): Promise { + const db = await this.getDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction('dataStore', 'readwrite'); + const store = transaction.objectStore('dataStore'); + const request = store.put(value, id); + + request.onsuccess = () => { + resolve(); + }; + + request.onerror = () => { + reject(request.error); + }; + }); + } + + static async get(id: string): Promise { + const db = await this.getDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction('dataStore', 'readonly'); + const store = transaction.objectStore('dataStore'); + const request = store.get(id); + + request.onsuccess = () => { + resolve(request.result); + }; + + request.onerror = () => { + reject(request.error); + }; + }); + } + + static async remove(id: string): Promise { + const db = await this.getDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction('dataStore', 'readwrite'); + const store = transaction.objectStore('dataStore'); + const request = store.delete(id); + + request.onsuccess = () => { + resolve(); + }; + + request.onerror = () => { + reject(request.error); + }; + }); + } +} + + + + +var CoverStorage:any +if (Platform.OS === "web") { + CoverStorage = CoverStorage_Web; +} + +export default CoverStorage + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d0d8c839335db48a6d8a527858af6164887f0f50..95eb08d17374f305552b1628c23095c60a300084 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@rneui/base": "^0.0.0-edge.2", "@rneui/themed": "^0.0.0-edge.2", "@shopify/flash-list": "1.6.4", + "@types/lodash": "^4.17.13", "axios": "^1.7.7", "dayjs": "^1.11.13", "disqus-react": "^1.1.5", @@ -8053,6 +8054,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index c5f61b4f5f374bfbf77e5f61a523491995749e94..8647ae696326e1aeb4028460362fdaaeee688291 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "@rneui/base": "^0.0.0-edge.2", "@rneui/themed": "^0.0.0-edge.2", "@shopify/flash-list": "1.6.4", + "@types/lodash": "^4.17.13", "axios": "^1.7.7", "dayjs": "^1.11.13", "disqus-react": "^1.1.5", diff --git a/requirements.txt b/requirements.txt index fcfa798089bc5b84728218d4a00059083a4b485b..ff55f3040f4d3b9406f85910721f73738166a021 100644 Binary files a/requirements.txt and b/requirements.txt differ