BloodyInside commited on
Commit
0ab8c90
β€’
1 Parent(s): 4af0f6c
Files changed (40) hide show
  1. backend/__pycache__/urls.cpython-312.pyc +0 -0
  2. backend/api/__pycache__/cloudflare_turnstile.cpython-312.pyc +0 -0
  3. backend/api/__pycache__/queue.cpython-312.pyc +0 -0
  4. backend/api/__pycache__/stream_file.cpython-312.pyc +0 -0
  5. backend/api/__pycache__/web_scrap.cpython-312.pyc +0 -0
  6. backend/api/cloudflare_turnstile.py +2 -2
  7. backend/api/queue.py +1 -1
  8. backend/api/stream_file.py +1 -1
  9. backend/api/web_scrap.py +4 -33
  10. backend/migrations/0002_remove_requestcache_room.py +17 -0
  11. backend/migrations/__pycache__/0002_remove_requestcache_room.cpython-312.pyc +0 -0
  12. backend/models/__pycache__/model_cache.cpython-312.pyc +0 -0
  13. backend/models/model_cache.py +0 -3
  14. backend/urls.py +0 -1
  15. core/__pycache__/middleware.cpython-312.pyc +0 -0
  16. core/middleware.py +1 -1
  17. frontend/app/_layout.tsx +7 -7
  18. frontend/app/explore/components/widgets.tsx +1 -0
  19. frontend/app/explore/index.tsx +9 -29
  20. frontend/app/explore/stylesheet/show_list_styles.tsx +2 -2
  21. frontend/app/index.tsx +1 -1
  22. frontend/app/read/[source]/[comic_id]/{index.tsx β†’ [chapter_idx].tsx} +32 -41
  23. frontend/app/read/components/chapter_image.tsx +129 -3
  24. frontend/app/read/modules/get_chapter.tsx +2 -1
  25. frontend/app/view/[source]/[comic_id].tsx +15 -11
  26. frontend/app/view/componenets/chapter.tsx +2 -2
  27. frontend/app/view/componenets/widgets/bookmark.tsx +1116 -0
  28. frontend/app/view/componenets/widgets/page_navigation.tsx +159 -0
  29. frontend/app/view/componenets/{widgets.tsx β†’ widgets/request_chapter.tsx} +7 -551
  30. frontend/components/Image.tsx +5 -1
  31. frontend/components/dropdown.tsx +2 -0
  32. frontend/components/menu/components/menu_button.tsx +27 -34
  33. frontend/components/menu/menu.tsx +95 -18
  34. frontend/components/menu/stylesheet/styles.tsx +16 -9
  35. frontend/components/navigation/TabBarIcon.tsx +0 -9
  36. frontend/constants/module/storages/chapter_data_storage.tsx +11 -8
  37. frontend/constants/module/storages/chapter_storage.tsx +8 -6
  38. frontend/constants/module/storages/comic_storage.tsx +13 -1
  39. frontend/constants/module/storages/image_cache_storage.tsx +4 -4
  40. frontend/constants/module/storages/storage.tsx +2 -1
backend/__pycache__/urls.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/urls.cpython-312.pyc and b/backend/__pycache__/urls.cpython-312.pyc differ
 
backend/api/__pycache__/cloudflare_turnstile.cpython-312.pyc CHANGED
Binary files a/backend/api/__pycache__/cloudflare_turnstile.cpython-312.pyc and b/backend/api/__pycache__/cloudflare_turnstile.cpython-312.pyc differ
 
backend/api/__pycache__/queue.cpython-312.pyc CHANGED
Binary files a/backend/api/__pycache__/queue.cpython-312.pyc and b/backend/api/__pycache__/queue.cpython-312.pyc differ
 
backend/api/__pycache__/stream_file.cpython-312.pyc CHANGED
Binary files a/backend/api/__pycache__/stream_file.cpython-312.pyc and b/backend/api/__pycache__/stream_file.cpython-312.pyc differ
 
backend/api/__pycache__/web_scrap.cpython-312.pyc CHANGED
Binary files a/backend/api/__pycache__/web_scrap.cpython-312.pyc and b/backend/api/__pycache__/web_scrap.cpython-312.pyc differ
 
backend/api/cloudflare_turnstile.py CHANGED
@@ -11,7 +11,7 @@ from ipware import get_client_ip
11
  env = environ.Env()
12
 
13
  @csrf_exempt
14
- @ratelimit(key='ip', rate='60/m')
15
  def verify(request):
16
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
17
  client_ip, is_routable = get_client_ip(request)
@@ -29,7 +29,7 @@ def verify(request):
29
  result = req.json()
30
  status = result.get("success")
31
  if (status):
32
-
33
  queryset = CloudflareTurnStileCache.objects.create(token=token)
34
  queryset.refresh_from_db()
35
  return JsonResponse(result)
 
11
  env = environ.Env()
12
 
13
  @csrf_exempt
14
+ @ratelimit(key='ip', rate='10/m')
15
  def verify(request):
16
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
17
  client_ip, is_routable = get_client_ip(request)
 
29
  result = req.json()
30
  status = result.get("success")
31
  if (status):
32
+
33
  queryset = CloudflareTurnStileCache.objects.create(token=token)
34
  queryset.refresh_from_db()
35
  return JsonResponse(result)
backend/api/queue.py CHANGED
@@ -18,7 +18,7 @@ env = environ.Env()
18
 
19
 
20
  @csrf_exempt
21
- @ratelimit(key='ip', rate='60/m')
22
  def request_chapter(request):
23
  try:
24
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
 
18
 
19
 
20
  @csrf_exempt
21
+ @ratelimit(key='ip', rate='10/m')
22
  def request_chapter(request):
23
  try:
24
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
backend/api/stream_file.py CHANGED
@@ -8,7 +8,7 @@ from backend.models.model_cache import SocketRequestChapterQueueCache, ComicStor
8
  import os, json, sys
9
 
10
  @csrf_exempt
11
- @ratelimit(key='ip', rate='60/m')
12
  def download_chapter(request):
13
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
14
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
 
8
  import os, json, sys
9
 
10
  @csrf_exempt
11
+ @ratelimit(key='ip', rate='30/m')
12
  def download_chapter(request):
13
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
14
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
backend/api/web_scrap.py CHANGED
@@ -18,7 +18,7 @@ env = environ.Env()
18
 
19
 
20
  @csrf_exempt
21
- @ratelimit(key='ip', rate='60/m')
22
  def get_list(request):
23
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
24
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
@@ -35,7 +35,7 @@ def get_list(request):
35
  return JsonResponse({"data":DATA})
36
 
37
 
38
- @ratelimit(key='ip', rate='60/m')
39
  def search(request):
40
  # if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
41
  try:
@@ -47,7 +47,7 @@ def search(request):
47
 
48
 
49
  @csrf_exempt
50
- @ratelimit(key='ip', rate='60/m')
51
  def get(request):
52
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
53
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
@@ -79,33 +79,4 @@ def get_cover(request,source,id,cover_id):
79
  except Exception as e:
80
  return HttpResponseBadRequest(str(e), status=500)
81
 
82
-
83
- def get_chapter(request):
84
- try:
85
- id = "manga-lo816008/1/410.html"
86
- job = web_scrap.source_control["colamanga"].get_chapter.scrap(id=id,output_dir=os.path.join(BASE_DIR,"media"))
87
- if job.get("status") == "success":
88
- chapter_id = id.split("/")[-1].split(".")[0]
89
- input_dir = os.path.join(BASE_DIR,"media",id.split("/")[0],chapter_id,"original")
90
- merged_output_dir = os.path.join(BASE_DIR,"media",id.split("/")[0],chapter_id,"merged")
91
- os.makedirs(merged_output_dir,exist_ok=True)
92
-
93
- manage_image.merge_images_vertically(input_dir=input_dir,output_dir=merged_output_dir,max_size=10*1024*1024)
94
-
95
- translated_merged_output_dir = os.path.join(BASE_DIR,"media",id.split("/")[0],chapter_id,"merged_translated")
96
- os.makedirs(translated_merged_output_dir,exist_ok=True)
97
-
98
-
99
- subprocess.run(
100
- ["python", "-m", "manga_translator", "-v", "--manga2eng", "--translator=m2m100_big", "-l", "ENG", "-i", f"{merged_output_dir}", "-o", f"{translated_merged_output_dir}"],
101
- cwd=os.path.join(BASE_DIR,"backend","module","utils","image_translator"), shell=True, check=True
102
- )
103
-
104
- translated_splited_output_dir = os.path.join(BASE_DIR,"media",id.split("/")[0],chapter_id,"translated")
105
-
106
- os.makedirs(translated_splited_output_dir,exist_ok=True)
107
- manage_image.split_image_vertically(input_dir=translated_merged_output_dir,output_dir=translated_splited_output_dir)
108
- except Exception as e:
109
- print(e)
110
-
111
- return JsonResponse({})
 
18
 
19
 
20
  @csrf_exempt
21
+ @ratelimit(key='ip', rate='20/m')
22
  def get_list(request):
23
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
24
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
 
35
  return JsonResponse({"data":DATA})
36
 
37
 
38
+ @ratelimit(key='ip', rate='20/m')
39
  def search(request):
40
  # if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
41
  try:
 
47
 
48
 
49
  @csrf_exempt
50
+ @ratelimit(key='ip', rate='20/m')
51
  def get(request):
52
  if request.method != "POST": return HttpResponseBadRequest('Allowed POST request only!', status=400)
53
  token = request.META.get('HTTP_X_CLOUDFLARE_TURNSTILE_TOKEN')
 
79
  except Exception as e:
80
  return HttpResponseBadRequest(str(e), status=500)
81
 
82
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/migrations/0002_remove_requestcache_room.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 5.1.1 on 2024-11-08 17:33
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('backend', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='requestcache',
15
+ name='room',
16
+ ),
17
+ ]
backend/migrations/__pycache__/0002_remove_requestcache_room.cpython-312.pyc ADDED
Binary file (616 Bytes). View file
 
backend/models/__pycache__/model_cache.cpython-312.pyc CHANGED
Binary files a/backend/models/__pycache__/model_cache.cpython-312.pyc and b/backend/models/__pycache__/model_cache.cpython-312.pyc differ
 
backend/models/model_cache.py CHANGED
@@ -5,11 +5,8 @@ import uuid
5
  def get_current_utc_time(): return date_utils.utc_time().get()
6
 
7
  class RequestCache(models.Model):
8
- room = models.TextField()
9
  client = models.UUIDField(primary_key=True)
10
  datetime = models.DateTimeField(default=get_current_utc_time)
11
-
12
-
13
 
14
  class CloudflareTurnStileCache(models.Model):
15
  token = models.TextField(primary_key=True)
 
5
  def get_current_utc_time(): return date_utils.utc_time().get()
6
 
7
  class RequestCache(models.Model):
 
8
  client = models.UUIDField(primary_key=True)
9
  datetime = models.DateTimeField(default=get_current_utc_time)
 
 
10
 
11
  class CloudflareTurnStileCache(models.Model):
12
  token = models.TextField(primary_key=True)
backend/urls.py CHANGED
@@ -18,7 +18,6 @@ urlpatterns = [
18
  path('web_scrap/search/', web_scrap.search),
19
  path('web_scrap/get/', web_scrap.get),
20
  path('web_scrap/get_cover/<str:source>/<str:id>/<str:cover_id>/', web_scrap.get_cover),
21
- path('web_scrap/get_chapter/', web_scrap.get_chapter),
22
 
23
 
24
 
 
18
  path('web_scrap/search/', web_scrap.search),
19
  path('web_scrap/get/', web_scrap.get),
20
  path('web_scrap/get_cover/<str:source>/<str:id>/<str:cover_id>/', web_scrap.get_cover),
 
21
 
22
 
23
 
core/__pycache__/middleware.cpython-312.pyc CHANGED
Binary files a/core/__pycache__/middleware.cpython-312.pyc and b/core/__pycache__/middleware.cpython-312.pyc differ
 
core/middleware.py CHANGED
@@ -35,7 +35,7 @@ class SequentialRequestMiddleware:
35
  def __call__(self, request):
36
  request_type = request.scope.get("type")
37
  request_path = request.path
38
-
39
  if request_type == "http":
40
 
41
  with TimeoutContext(30) as executor:
 
35
  def __call__(self, request):
36
  request_type = request.scope.get("type")
37
  request_path = request.path
38
+ print(request_path)
39
  if request_type == "http":
40
 
41
  with TimeoutContext(30) as executor:
frontend/app/_layout.tsx CHANGED
@@ -70,7 +70,7 @@ SplashScreen.preventAutoHideAsync();
70
  export default function RootLayout() {
71
  const pathname = usePathname()
72
  const Dimensions = useWindowDimensions();
73
- const [showMenuContext,setShowMenuContext]:any = useState(true)
74
  const [themeTypeContext,setThemeTypeContext]:any = useState("")
75
  const [apiBaseContext, setApiBaseContext]:any = useState("")
76
  const [socketBaseContext, setSocketBaseContext]:any = useState("")
@@ -134,7 +134,7 @@ return (<>{loaded && themeTypeContext && apiBaseContext && socketBaseContext &&
134
  widgetContext, setWidgetContext,
135
  showCloudflareTurnstileContext, setShowCloudflareTurnstileContext,
136
  }}>
137
- <View style={{width:"100%",height:"100%",backgroundColor: Theme[themeTypeContext].background_color}}>
138
  {showCloudflareTurnstileContext
139
  ? <View style={{position:"absolute",width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",backgroundColor:Theme[themeTypeContext].background_color}}>
140
  <CloudflareTurnstile
@@ -167,17 +167,17 @@ return (<>{loaded && themeTypeContext && apiBaseContext && socketBaseContext &&
167
  height:"100%",
168
  display: 'flex',
169
  flex: 1,
170
- flexDirection: Dimensions.width <= 720 ? 'column' : 'row-reverse',
171
 
172
  }}>
173
 
174
  <Stack screenOptions={{ headerShown: false}}>
175
- <Stack.Screen name="index"/>
176
-
177
- <Stack.Screen name="+not-found" />
178
  </Stack>
179
  <AnimatePresence exitBeforeEnter>
180
- {showMenuContext && <MemoMenu/>}
181
  </AnimatePresence>
182
  </View>
183
  </>
 
70
  export default function RootLayout() {
71
  const pathname = usePathname()
72
  const Dimensions = useWindowDimensions();
73
+ const [showMenuContext, setShowMenuContext]:any = useState(false)
74
  const [themeTypeContext,setThemeTypeContext]:any = useState("")
75
  const [apiBaseContext, setApiBaseContext]:any = useState("")
76
  const [socketBaseContext, setSocketBaseContext]:any = useState("")
 
134
  widgetContext, setWidgetContext,
135
  showCloudflareTurnstileContext, setShowCloudflareTurnstileContext,
136
  }}>
137
+ <View style={{width:Dimensions.width,height:Dimensions.height,backgroundColor: Theme[themeTypeContext].background_color}}>
138
  {showCloudflareTurnstileContext
139
  ? <View style={{position:"absolute",width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",backgroundColor:Theme[themeTypeContext].background_color}}>
140
  <CloudflareTurnstile
 
167
  height:"100%",
168
  display: 'flex',
169
  flex: 1,
170
+ flexDirection: 'row-reverse',
171
 
172
  }}>
173
 
174
  <Stack screenOptions={{ headerShown: false}}>
175
+ <Stack.Screen name="index"/>
176
+
177
+ <Stack.Screen name="+not-found" />
178
  </Stack>
179
  <AnimatePresence exitBeforeEnter>
180
+ {showMenuContext !== null && <MemoMenu/>}
181
  </AnimatePresence>
182
  </View>
183
  </>
frontend/app/explore/components/widgets.tsx CHANGED
@@ -18,6 +18,7 @@ import ChapterStorage from '@/constants/module/storages/chapter_storage';
18
 
19
  export const PageNavigationWidget = ({setPage}:any) =>{
20
  const Dimensions = useWindowDimensions();
 
21
 
22
  const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
23
  const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
 
18
 
19
  export const PageNavigationWidget = ({setPage}:any) =>{
20
  const Dimensions = useWindowDimensions();
21
+
22
 
23
  const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
24
  const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
frontend/app/explore/index.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useEffect, useState, useCallback, useContext, useRef } from 'react';
2
- import { Link, router } from 'expo-router';
3
  import Image from '@/components/Image';
4
  import { StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl } from 'react-native';
5
  import { SafeAreaView } from 'react-native-safe-area-context';
@@ -45,9 +45,15 @@ const Index = ({}:any) => {
45
  const controller = new AbortController();
46
  const signal = controller.signal;
47
 
 
 
 
 
 
 
 
48
  useEffect(() => {
49
  (async ()=>{
50
-
51
  setStyles(__styles(themeTypeContext,Dimensions))
52
 
53
  let __translate:any = await Storage.get("explore_translate")
@@ -69,7 +75,6 @@ const Index = ({}:any) => {
69
 
70
  const onRefresh = () => {
71
  if (!(styles && themeTypeContext && apiBaseContext)) return
72
- setShowMenuContext(true)
73
  setIsLoading(true);
74
  SET_CONTENT([])
75
  get_list(setShowCloudflareTurnstileContext,setFeedBack,signal,setIsLoading,translate,SET_CONTENT,search,page)
@@ -81,29 +86,6 @@ const Index = ({}:any) => {
81
  },[page])
82
 
83
 
84
- const onScroll = useCallback((event:any) => {
85
- const nativeEvent = event.nativeEvent
86
- const { layoutMeasurement, contentOffset, contentSize } = nativeEvent;
87
- var currentOffset = event.nativeEvent.contentOffset.y;
88
- var direction = currentOffset > scrollOffset.current ? 'down' : 'up';
89
- scrollOffset.current = currentOffset;
90
- if (direction === 'down') {
91
- if (contentOffset.y <= contentSize.height*0.025) {
92
- setShowMenuContext(true)
93
- }else{
94
- setShowMenuContext(false)
95
- }
96
- }
97
- else {
98
-
99
- if (layoutMeasurement.height + contentOffset.y >= (contentSize.height - contentSize.height*0.025)) {
100
- setShowMenuContext(false)
101
- }else{
102
- setShowMenuContext(true)
103
- }
104
- }
105
-
106
- },[])
107
 
108
 
109
 
@@ -114,8 +96,6 @@ const Index = ({}:any) => {
114
  if (!isLoading) onRefresh()
115
  }} />
116
  }
117
- onScroll={(event) => {onScroll(event)}}
118
- scrollEventThrottle={5}
119
  >
120
 
121
  <View style={styles.header_container}>
@@ -409,7 +389,7 @@ const Index = ({}:any) => {
409
  {CONTENT.map((item:any,index:number)=>(
410
  <TouchableRipple
411
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
412
- onPress={()=>{router.push(`/view/${source}/${item.id}`)}} key={index}
413
  style={styles.item_box}
414
  >
415
  <>
 
1
  import React, { useEffect, useState, useCallback, useContext, useRef } from 'react';
2
+ import { Link, router, useFocusEffect } from 'expo-router';
3
  import Image from '@/components/Image';
4
  import { StyleSheet, useWindowDimensions, ScrollView, Pressable, RefreshControl } from 'react-native';
5
  import { SafeAreaView } from 'react-native-safe-area-context';
 
45
  const controller = new AbortController();
46
  const signal = controller.signal;
47
 
48
+ useFocusEffect(useCallback(() => {
49
+ setShowMenuContext(true)
50
+ return () => {
51
+
52
+ };
53
+ }, []))
54
+
55
  useEffect(() => {
56
  (async ()=>{
 
57
  setStyles(__styles(themeTypeContext,Dimensions))
58
 
59
  let __translate:any = await Storage.get("explore_translate")
 
75
 
76
  const onRefresh = () => {
77
  if (!(styles && themeTypeContext && apiBaseContext)) return
 
78
  setIsLoading(true);
79
  SET_CONTENT([])
80
  get_list(setShowCloudflareTurnstileContext,setFeedBack,signal,setIsLoading,translate,SET_CONTENT,search,page)
 
86
  },[page])
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
 
91
 
 
96
  if (!isLoading) onRefresh()
97
  }} />
98
  }
 
 
99
  >
100
 
101
  <View style={styles.header_container}>
 
389
  {CONTENT.map((item:any,index:number)=>(
390
  <TouchableRipple
391
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
392
+ onPress={()=>{router.navigate(`/view/${source}/${item.id}`)}} key={index}
393
  style={styles.item_box}
394
  >
395
  <>
frontend/app/explore/stylesheet/show_list_styles.tsx CHANGED
@@ -92,13 +92,13 @@ export const __styles:any = (theme_type:string,Dimensions:any) => {
92
  alignItems:"center",
93
  gap:15,
94
  height:"auto",
95
- width:Math.max(((Dimensions.width+Dimensions.height)/2)*0.25,100),
96
  borderRadius:8,
97
 
98
  },
99
  item_cover:{
100
  width:"100%",
101
- height:Math.max(((Dimensions.width+Dimensions.height)/2)*0.4,125),
102
  borderRadius:8,
103
  shadowColor: "#000",
104
  shadowOffset: {
 
92
  alignItems:"center",
93
  gap:15,
94
  height:"auto",
95
+ width:Math.max(((Dimensions.width+Dimensions.height)/2)*0.225,100),
96
  borderRadius:8,
97
 
98
  },
99
  item_cover:{
100
  width:"100%",
101
+ height:Math.max(((Dimensions.width+Dimensions.height)/2)*0.325,125),
102
  borderRadius:8,
103
  shadowColor: "#000",
104
  shadowOffset: {
frontend/app/index.tsx CHANGED
@@ -9,7 +9,7 @@ const Index = () => {
9
  const pathname = usePathname()
10
 
11
  if (pathname === "/" || pathname === "") return (
12
- <Redirect href="/read/colamanga/manga-wp55334?idx=985" />
13
  )
14
 
15
  }
 
9
  const pathname = usePathname()
10
 
11
  if (pathname === "/" || pathname === "") return (
12
+ <Redirect href="/view/colamanga/manga-wp55334" />
13
  )
14
 
15
  }
frontend/app/read/[source]/[comic_id]/{index.tsx β†’ [chapter_idx].tsx} RENAMED
@@ -49,12 +49,14 @@ const Index = ({}:any) => {
49
  const [isAdding, setIsAdding]:any = useState(false)
50
  const [zoom, setZoom]:any = useState(0)
51
 
52
- const CHAPTER_IDX = useRef(Number(useLocalSearchParams().idx as string));
53
 
54
 
55
- useEffect(()=>{
56
- setShowMenuContext(false)
57
- },[])
 
 
58
 
59
  // First Load
60
  useEffect(()=>{(async () => {
@@ -91,36 +93,39 @@ const Index = ({}:any) => {
91
  },[]))
92
 
93
  const renderItem = useCallback(({item,index}:any) => {
94
- return <ChapterImage key={index} item={item} zoom={zoom} showOptions={showOptions} setShowOptions={setShowOptions}/>
95
  },[zoom,showOptions,setShowOptions])
96
 
97
  const onViewableItemsChanged = useCallback(async ({viewableItems, changed}:any) => {
98
- const expect_chapter_idx = [CHAPTER_IDX.current + 1, CHAPTER_IDX.current - 1]
99
- const current_count = viewableItems.filter((data:any) => data.item.chapter_idx === CHAPTER_IDX.current).length
100
- const existed_count = viewableItems.filter((data:any) => expect_chapter_idx.includes(data.item.chapter_idx)).length
101
 
102
- if (current_count || existed_count){
103
- const choose_idx = current_count > existed_count ? CHAPTER_IDX.current : viewableItems.find((data:any) => expect_chapter_idx.includes(data.item.chapter_idx))?.item.idx
104
- if (choose_idx === CHAPTER_IDX.current) return
105
- const stored_chapter = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,choose_idx, {exclude_fields:["data"]})
106
- setChapterInfo({
107
- chapter_id: stored_chapter?.id,
108
- chapter_idx: stored_chapter?.id,
109
- title: stored_chapter?.title,
110
- })
111
- const stored_comic = await ComicStorage.getByID(SOURCE, COMIC_ID)
112
- if (stored_comic.history.idx && choose_idx > stored_comic.history.idx) {
113
- await ComicStorage.updateHistory(SOURCE, COMIC_ID, {idx:stored_chapter?.idx,id:stored_chapter?.id,title:stored_chapter?.title})
114
- }
115
- router.setParams({idx:choose_idx})
116
- CHAPTER_IDX.current = choose_idx
117
- }
118
  },[])
119
 
120
  const onEndReached = useCallback(async () => {
121
- const chapter_current_data = await get_chapter(SOURCE,COMIC_ID,CHAPTER_IDX.current+1)
 
 
 
122
 
123
- SET_DATA([...DATA,...chapter_current_data])
124
  },[DATA])
125
 
126
  return (<>
@@ -218,26 +223,12 @@ const Index = ({}:any) => {
218
  <FlatList
219
  data={DATA}
220
  renderItem={renderItem}
221
- onEndReachedThreshold={0.5}
222
  windowSize={21}
223
  ItemSeparatorComponent={undefined}
224
  onEndReached={onEndReached}
225
  onViewableItemsChanged={onViewableItemsChanged}
226
  />
227
- {isAdding && (
228
- <View
229
- style={{
230
- display:"flex",
231
- width:"100%",
232
- height:"auto",
233
- justifyContent:"center",
234
- padding:16,
235
- }}
236
- >
237
- <ActivityIndicator animating={true} size={(0.03 * ((Dimensions.width+Dimensions.height)/2)) * (1 - zoom/100)}/>
238
- </View>
239
- )}
240
-
241
  </View>
242
  <AnimatePresence exitBeforeEnter>
243
  {showOptions.state &&
 
49
  const [isAdding, setIsAdding]:any = useState(false)
50
  const [zoom, setZoom]:any = useState(0)
51
 
52
+ const CHAPTER_IDX = useRef(Number(useLocalSearchParams().chapter_idx as string));
53
 
54
 
55
+ useFocusEffect(useCallback(() => {
56
+ setShowMenuContext(null)
57
+ return () => {
58
+ }
59
+ },[]))
60
 
61
  // First Load
62
  useEffect(()=>{(async () => {
 
93
  },[]))
94
 
95
  const renderItem = useCallback(({item,index}:any) => {
96
+ return <ChapterImage key={index} item={item} zoom={zoom} showOptions={showOptions} setShowOptions={setShowOptions} setIsLoading={setIsLoading} SET_DATA={SET_DATA}/>
97
  },[zoom,showOptions,setShowOptions])
98
 
99
  const onViewableItemsChanged = useCallback(async ({viewableItems, changed}:any) => {
100
+ // const expect_chapter_idx = [CHAPTER_IDX.current + 1, CHAPTER_IDX.current - 1]
101
+ // const current_count = viewableItems.filter((data:any) => data.item.chapter_idx === CHAPTER_IDX.current).length
102
+ // const existed_count = viewableItems.filter((data:any) => expect_chapter_idx.includes(data.item.chapter_idx)).length
103
 
104
+ // if (current_count || existed_count){
105
+ // const choose_idx = current_count > existed_count ? CHAPTER_IDX.current : viewableItems.find((data:any) => expect_chapter_idx.includes(data.item.chapter_idx))?.item.chapter_idx
106
+ // if (choose_idx === CHAPTER_IDX.current) return
107
+ // const stored_chapter = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,choose_idx, {exclude_fields:["data"]})
108
+ // setChapterInfo({
109
+ // chapter_id: stored_chapter?.id,
110
+ // chapter_idx: stored_chapter?.id,
111
+ // title: stored_chapter?.title,
112
+ // })
113
+ // const stored_comic = await ComicStorage.getByID(SOURCE, COMIC_ID)
114
+ // if (stored_comic.history.idx && choose_idx > stored_comic.history.idx) {
115
+ // await ComicStorage.updateHistory(SOURCE, COMIC_ID, {idx:stored_chapter?.idx,id:stored_chapter?.id,title:stored_chapter?.title})
116
+ // }
117
+ // router.setParams({idx:choose_idx})
118
+ // CHAPTER_IDX.current = choose_idx
119
+ // }
120
  },[])
121
 
122
  const onEndReached = useCallback(async () => {
123
+ console.log(DATA)
124
+ // const NEW_DATA = DATA.filter((data:any) => data.chapter_idx === CHAPTER_IDX.current-2)
125
+
126
+ // const chapter_current_data = await get_chapter(SOURCE,COMIC_ID,CHAPTER_IDX.current+1)
127
 
128
+ // SET_DATA([...NEW_DATA,...chapter_current_data])
129
  },[DATA])
130
 
131
  return (<>
 
223
  <FlatList
224
  data={DATA}
225
  renderItem={renderItem}
226
+ // onEndReachedThreshold={0.5}
227
  windowSize={21}
228
  ItemSeparatorComponent={undefined}
229
  onEndReached={onEndReached}
230
  onViewableItemsChanged={onViewableItemsChanged}
231
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  </View>
233
  <AnimatePresence exitBeforeEnter>
234
  {showOptions.state &&
frontend/app/read/components/chapter_image.tsx CHANGED
@@ -20,8 +20,9 @@ import Image from '@/components/Image';
20
  import {CONTEXT} from '@/constants/module/context';
21
  import {blobToBase64, base64ToBlob} from "@/constants/module/file_manager";
22
  import Theme from '@/constants/theme';
 
23
 
24
- const ChapterImage = ({item, zoom, showOptions,setShowOptions}:any)=>{
25
  const SOURCE = useLocalSearchParams().source;
26
  const COMIC_ID = useLocalSearchParams().comic_id;
27
  const CHAPTER_IDX = Number(useLocalSearchParams().chapter_idx as string);
@@ -140,7 +141,7 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions}:any)=>{
140
  fontFamily:"roboto-bold",
141
  }}
142
  >
143
- No more available chapters on local.
144
  </Text>
145
  <Text selectable={false}
146
  numberOfLines={1}
@@ -150,10 +151,135 @@ const ChapterImage = ({item, zoom, showOptions,setShowOptions}:any)=>{
150
  fontFamily:"roboto-bold",
151
  }}
152
  >
153
- You can go back and download more.
154
  </Text>
155
  </View>
156
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  </>)
158
  : (
159
  <View
 
20
  import {CONTEXT} from '@/constants/module/context';
21
  import {blobToBase64, base64ToBlob} from "@/constants/module/file_manager";
22
  import Theme from '@/constants/theme';
23
+ import { get_chapter } from '../modules/get_chapter';
24
 
25
+ const ChapterImage = ({item, zoom, showOptions,setShowOptions, setIsLoading, SET_DATA}:any)=>{
26
  const SOURCE = useLocalSearchParams().source;
27
  const COMIC_ID = useLocalSearchParams().comic_id;
28
  const CHAPTER_IDX = Number(useLocalSearchParams().chapter_idx as string);
 
141
  fontFamily:"roboto-bold",
142
  }}
143
  >
144
+ No more chapters on local.
145
  </Text>
146
  <Text selectable={false}
147
  numberOfLines={1}
 
151
  fontFamily:"roboto-bold",
152
  }}
153
  >
154
+ You can go back and download more if available.
155
  </Text>
156
  </View>
157
  )}
158
+
159
+ {item.type === "chapter-navigate" && (
160
+
161
+ <View
162
+ style={{
163
+ display:"flex",
164
+ flexDirection:"row",
165
+ justifyContent:"space-between",
166
+ alignItems:"center",
167
+ width:Dimensions.width > 720
168
+ ? 0.8 * Dimensions.width * (1 - zoom / 100)
169
+ : `${100 - zoom}%`,
170
+ paddingHorizontal: 12,
171
+ paddingVertical: 18,
172
+ }}
173
+ >
174
+ <TouchableRipple
175
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
176
+ style={{
177
+ width:"auto",
178
+ display:"flex",
179
+ flexDirection:"column",
180
+ justifyContent:"center",
181
+ alignSelf:"center",
182
+ padding:8,
183
+ paddingHorizontal:18,
184
+ borderRadius:Dimensions.width*0.65/2,
185
+ backgroundColor:Theme[themeTypeContext].border_color,
186
+
187
+ shadowColor: Theme[themeTypeContext].shadow_color,
188
+ shadowOffset: { width: 0, height: 2 },
189
+ shadowOpacity: 0.25,
190
+ shadowRadius: 3.84,
191
+ elevation: 5,
192
+
193
+ }}
194
+ onPress={async ()=>{
195
+ const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx-1)
196
+ if (stored_chapter_info?.data_state === "completed"){
197
+ router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`)
198
+ }else{
199
+ Toast.show({
200
+ type: 'info',
201
+ text1: 'Chapter not download yet.',
202
+ text2: "You can go back and download more.",
203
+
204
+ position: "bottom",
205
+ visibilityTime: 4000,
206
+ text1Style:{
207
+ fontFamily:"roboto-bold",
208
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
209
+ },
210
+ text2Style:{
211
+ fontFamily:"roboto-medium",
212
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
213
+
214
+ },
215
+ });
216
+ }
217
+ }}
218
+ >
219
+ <Text selectable={false}
220
+ style={{
221
+ color:Theme[themeTypeContext].text_color,
222
+ fontFamily:"roboto-medium",
223
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03
224
+ }}
225
+ >Previous</Text>
226
+ </TouchableRipple>
227
+
228
+ <TouchableRipple
229
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
230
+ style={{
231
+ width:"auto",
232
+ display:"flex",
233
+ flexDirection:"column",
234
+ justifyContent:"center",
235
+ alignSelf:"center",
236
+ padding:8,
237
+ paddingHorizontal:18,
238
+ borderRadius:Dimensions.width*0.65/2,
239
+ backgroundColor:Theme[themeTypeContext].border_color,
240
+
241
+ shadowColor: Theme[themeTypeContext].shadow_color,
242
+ shadowOffset: { width: 0, height: 2 },
243
+ shadowOpacity: 0.25,
244
+ shadowRadius: 3.84,
245
+ elevation: 5,
246
+ }}
247
+ onPress={async ()=>{
248
+ const stored_chapter_info = await ChapterStorage.getByIdx(`${SOURCE}-${COMIC_ID}`,item.chapter_idx+1)
249
+ if (stored_chapter_info?.data_state === "completed"){
250
+ router.replace(`/read/${SOURCE}/${COMIC_ID}/${stored_chapter_info.idx}/`)
251
+ }else{
252
+ Toast.show({
253
+ type: 'info',
254
+ text1: 'Chapter not download yet.',
255
+ text2: "You can go back and download more.",
256
+
257
+ position: "bottom",
258
+ visibilityTime: 4000,
259
+ text1Style:{
260
+ fontFamily:"roboto-bold",
261
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
262
+ },
263
+ text2Style:{
264
+ fontFamily:"roboto-medium",
265
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
266
+
267
+ },
268
+ });
269
+ }
270
+ }}
271
+ >
272
+ <Text selectable={false}
273
+ style={{
274
+ color:Theme[themeTypeContext].text_color,
275
+ fontFamily:"roboto-medium",
276
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03
277
+ }}
278
+ >Next</Text>
279
+ </TouchableRipple>
280
+ </View>
281
+
282
+ )}
283
  </>)
284
  : (
285
  <View
frontend/app/read/modules/get_chapter.tsx CHANGED
@@ -22,10 +22,11 @@ export const get_chapter = async (
22
  DATA.push({type:"page",id:`${SOURCE}-${COMIC_ID}-${CHAPTER_IDX}-${i}`, chapter_idx: CHAPTER_IDX})
23
  }
24
  if (next_stored_chapter) {
25
- DATA.push({type:"chapter-info-banner", value:{last:current_stored_chapter.title, next:next_stored_chapter.title}})
26
  }else{
27
  DATA.push({type:"no-chapter-banner"})
28
  }
 
29
  return DATA
30
  }else{
31
  return []
 
22
  DATA.push({type:"page",id:`${SOURCE}-${COMIC_ID}-${CHAPTER_IDX}-${i}`, chapter_idx: CHAPTER_IDX})
23
  }
24
  if (next_stored_chapter) {
25
+ DATA.push({type:"chapter-info-banner", value:{last:current_stored_chapter.title, next:next_stored_chapter.title}, chapter_idx: CHAPTER_IDX})
26
  }else{
27
  DATA.push({type:"no-chapter-banner"})
28
  }
29
+ DATA.push({type:"chapter-navigate", chapter_idx: CHAPTER_IDX})
30
  return DATA
31
  }else{
32
  return []
frontend/app/view/[source]/[comic_id].tsx CHANGED
@@ -22,7 +22,10 @@ import ChapterStorage from '@/constants/module/storages/chapter_storage';
22
  import ComicStorage from '@/constants/module/storages/comic_storage';
23
  import { CONTEXT } from '@/constants/module/context';
24
  import Dropdown from '@/components/dropdown';
25
- import { PageNavigationWidget, RequestChapterWidget, BookmarkWidget } from '../componenets/widgets';
 
 
 
26
  import ChapterComponent from '../componenets/chapter';
27
 
28
 
@@ -81,6 +84,7 @@ const Index = ({}:any) => {
81
  // Test Section
82
  useEffect(() => {
83
  // console.log(CONTENT)
 
84
  },[CONTENT])
85
 
86
 
@@ -153,8 +157,9 @@ const Index = ({}:any) => {
153
  }
154
  },[]))
155
 
156
- // Clean up on unmount
157
  useFocusEffect(useCallback(() => {
 
158
  return () => {
159
  controller.abort();
160
  }
@@ -226,14 +231,13 @@ const Index = ({}:any) => {
226
  // First load managing
227
  useEffect(() => {
228
  (async ()=>{
229
- setShowMenuContext(false)
230
  setStyles(__styles(themeTypeContext,Dimensions))
231
 
232
- let __translate:any = await Storage.get("explore_show_translate")
233
 
234
  if (!__translate) {
235
  __translate = {state:false,from:"auto",to:"en"}
236
- await Storage.store("explore_show_translate",__translate)
237
  }else __translate = __translate
238
 
239
  setTranslate(__translate)
@@ -438,7 +442,7 @@ const Index = ({}:any) => {
438
  value={translate.from}
439
  onChange={async (item:any) => {
440
  setTranslate({...translate,from:item.value})
441
- await Storage.store("explore_show_translate",{...translate,from:item.value})
442
  }}
443
  />
444
  </View>
@@ -456,7 +460,7 @@ const Index = ({}:any) => {
456
  value={translate.to}
457
  onChange={async (item:any) => {
458
  setTranslate({...translate,to:item.value})
459
- await Storage.store("explore_show_translate",{...translate,to:item.value})
460
  }}
461
  />
462
  </View>
@@ -477,10 +481,10 @@ const Index = ({}:any) => {
477
  onPress={async () => {
478
  if (translate.state){
479
  setTranslate({...translate,state:false})
480
- await Storage.store("explore_show_translate",{...translate,state:false})
481
  }else{
482
  setTranslate({...translate,state:true})
483
- await Storage.store("explore_show_translate",{...translate,state:true})
484
  }
485
 
486
  }}
@@ -641,7 +645,7 @@ const Index = ({}:any) => {
641
 
642
  }}
643
  onPress={()=>{
644
- router.push(`/read/${SOURCE}/${ID}/?idx=${history.idx}`)
645
  }}
646
  >
647
  <View
@@ -707,7 +711,7 @@ const Index = ({}:any) => {
707
  console.log(stored_chapter)
708
  if (stored_chapter.data_state === "completed"){
709
  await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
710
- router.push(`/read/${SOURCE}/${ID}/?idx=${stored_chapter.idx}`)
711
  }else{
712
  Toast.show({
713
  type: 'error',
 
22
  import ComicStorage from '@/constants/module/storages/comic_storage';
23
  import { CONTEXT } from '@/constants/module/context';
24
  import Dropdown from '@/components/dropdown';
25
+ import PageNavigationWidget from '../componenets/widgets/page_navigation';
26
+ import RequestChapterWidget from '../componenets/widgets/request_chapter';
27
+ import BookmarkWidget from '../componenets/widgets/bookmark';
28
+
29
  import ChapterComponent from '../componenets/chapter';
30
 
31
 
 
84
  // Test Section
85
  useEffect(() => {
86
  // console.log(CONTENT)
87
+
88
  },[CONTENT])
89
 
90
 
 
157
  }
158
  },[]))
159
 
160
+ // Clean up on mount/unmount
161
  useFocusEffect(useCallback(() => {
162
+ setShowMenuContext(null)
163
  return () => {
164
  controller.abort();
165
  }
 
231
  // First load managing
232
  useEffect(() => {
233
  (async ()=>{
 
234
  setStyles(__styles(themeTypeContext,Dimensions))
235
 
236
+ let __translate:any = await Storage.get("view_show_translate")
237
 
238
  if (!__translate) {
239
  __translate = {state:false,from:"auto",to:"en"}
240
+ await Storage.store("view_show_translate",__translate)
241
  }else __translate = __translate
242
 
243
  setTranslate(__translate)
 
442
  value={translate.from}
443
  onChange={async (item:any) => {
444
  setTranslate({...translate,from:item.value})
445
+ await Storage.store("view_show_translate",{...translate,from:item.value})
446
  }}
447
  />
448
  </View>
 
460
  value={translate.to}
461
  onChange={async (item:any) => {
462
  setTranslate({...translate,to:item.value})
463
+ await Storage.store("view_show_translate",{...translate,to:item.value})
464
  }}
465
  />
466
  </View>
 
481
  onPress={async () => {
482
  if (translate.state){
483
  setTranslate({...translate,state:false})
484
+ await Storage.store("view_show_translate",{...translate,state:false})
485
  }else{
486
  setTranslate({...translate,state:true})
487
+ await Storage.store("view_show_translate",{...translate,state:true})
488
  }
489
 
490
  }}
 
645
 
646
  }}
647
  onPress={()=>{
648
+ router.push(`/read/${SOURCE}/${ID}/${history.idx}/`)
649
  }}
650
  >
651
  <View
 
711
  console.log(stored_chapter)
712
  if (stored_chapter.data_state === "completed"){
713
  await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
714
+ router.push(`/read/${SOURCE}/${ID}/${stored_chapter.idx}/`)
715
  }else{
716
  Toast.show({
717
  type: 'error',
frontend/app/view/componenets/chapter.tsx CHANGED
@@ -22,7 +22,7 @@ import ChapterStorage from '@/constants/module/storages/chapter_storage';
22
  import ComicStorage from '@/constants/module/storages/comic_storage';
23
  import { CONTEXT } from '@/constants/module/context';
24
  import Dropdown from '@/components/dropdown';
25
- import { RequestChapterWidget, BookmarkWidget } from './widgets';
26
 
27
 
28
  import { get, store_comic_cover, get_requested_info } from '../modules/content'
@@ -134,7 +134,7 @@ const ChapterComponent = ({
134
  if (stored_chapter?.data_state === "completed") {
135
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
136
  if (!stored_comic.history.idx || chapter.idx > stored_comic.history.idx) await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
137
- router.push(`/read/${SOURCE}/${ID}/?idx=${chapter.idx}`)
138
  }else{
139
  Toast.show({
140
  type: 'error',
 
22
  import ComicStorage from '@/constants/module/storages/comic_storage';
23
  import { CONTEXT } from '@/constants/module/context';
24
  import Dropdown from '@/components/dropdown';
25
+ import RequestChapterWidget from './widgets/request_chapter';
26
 
27
 
28
  import { get, store_comic_cover, get_requested_info } from '../modules/content'
 
134
  if (stored_chapter?.data_state === "completed") {
135
  const stored_comic = await ComicStorage.getByID(SOURCE,ID)
136
  if (!stored_comic.history.idx || chapter.idx > stored_comic.history.idx) await ComicStorage.updateHistory(SOURCE,ID,{idx:chapter.idx, id:chapter.id, title:chapter.title})
137
+ router.push(`/read/${SOURCE}/${ID}/${chapter.idx}/`)
138
  }else{
139
  Toast.show({
140
  type: 'error',
frontend/app/view/componenets/widgets/bookmark.tsx ADDED
@@ -0,0 +1,1116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useEffect, useState, useCallback, useContext, useRef, Fragment } from 'react';
3
+ import { Platform, useWindowDimensions, ScrollView } from 'react-native';
4
+
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper';
6
+ import { View, AnimatePresence } from 'moti';
7
+ import Toast from 'react-native-toast-message';
8
+ import * as FileSystem from 'expo-file-system';
9
+ import axios from 'axios';
10
+
11
+
12
+ import Theme from '@/constants/theme';
13
+ import Dropdown from '@/components/dropdown';
14
+ import { CONTEXT } from '@/constants/module/context';
15
+ import { store_comic_cover } from '../../modules/content';
16
+ import Storage from '@/constants/module/storages/storage';
17
+ import ComicStorage from '@/constants/module/storages/comic_storage';
18
+ import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
19
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
20
+
21
+
22
+ interface BookmarkWidgetProps {
23
+ onRefresh: any;
24
+ SOURCE: string | string[];
25
+ ID: string | string[];
26
+ CONTENT: any;
27
+ }
28
+
29
+ const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
30
+ onRefresh,
31
+ SOURCE,
32
+ ID,
33
+ CONTENT
34
+ }) => {
35
+ const Dimensions = useWindowDimensions();
36
+
37
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
38
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
39
+ const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
40
+ const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
41
+
42
+ const [BOOKMARK_DATA, SET_BOOKMARK_DATA]: any = useState([])
43
+ const [MIGRATE_BOOKMARK_DATA, SET_MIGRATE_BOOKMARK_DATA]:any = useState([])
44
+
45
+
46
+ const [showMenuOption, setShowMenuOption]:any = useState({state:false,positions:[0,0,0,0],id:""})
47
+ const [searchTag, setSearchTag]:any = useState("")
48
+
49
+ const [migrateTag,setMigrateTag]:any = useState("")
50
+
51
+ const [defaultTag, setDefaultTag]:any = useState("")
52
+ const [bookmark, setBookmark]:any = useState("")
53
+ const [manageBookmark, setManageBookmark]:any = useState({state:false,edit:"",delete:""})
54
+ const [createTag, setCreateTag]:any = useState({state:false,title:""})
55
+ const [removeTag, setRemoveTag]:any = useState({state:false, removing: false})
56
+
57
+
58
+ const controller = new AbortController();
59
+ const signal = controller.signal;
60
+
61
+ const RenderTag = ({item}:any) =>{
62
+ const [editTag, setEditTag]:any = useState(item.value)
63
+ return (<>
64
+ {item.value.includes(searchTag) &&
65
+ (
66
+ <View
67
+ style={{
68
+ display:"flex",
69
+ flexDirection:"row",
70
+ alignItems:"center",
71
+ justifyContent:"space-between",
72
+ gap:8,
73
+ zIndex:10,
74
+ }}
75
+ >
76
+ <>{manageBookmark.edit !== item.value && manageBookmark.delete !== item.value &&
77
+ (<View
78
+ style={{
79
+ width:"100%",
80
+ display:"flex",
81
+ flexDirection:"row",
82
+ justifyContent:"space-between",
83
+ alignItems:"center",
84
+ height:"auto",
85
+ gap:18,
86
+ }}
87
+ >
88
+ <Text
89
+ style={{
90
+ color:"white",
91
+ fontFamily:"roboto-medium",
92
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.025
93
+ }}
94
+ >{item.label}</Text>
95
+ <View
96
+ style={{
97
+ width:"auto",
98
+ height:"auto",
99
+
100
+ }}
101
+ >
102
+
103
+ <TouchableRipple
104
+
105
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
106
+ style={{
107
+ borderRadius:5,
108
+ borderWidth:0,
109
+ backgroundColor: "transparent",
110
+ padding:5,
111
+
112
+ }}
113
+
114
+ onPress={(event)=>{
115
+ if (manageBookmark.edit){
116
+ setManageBookmark({...manageBookmark,edit:""})
117
+ setEditTag("")
118
+ }
119
+
120
+
121
+ const x = event.nativeEvent.pageX
122
+ const y = event.nativeEvent.pageY
123
+
124
+ setShowMenuOption({
125
+ ...showMenuOption,
126
+ state: showMenuOption.id === item.value ? false : true,
127
+ positions:[y+((Dimensions.width+Dimensions.height)/2)*0.0225,0,x-((Dimensions.width+Dimensions.height)/2)*0.18,0],
128
+ id:showMenuOption.id === item.value ? "" : item.value,
129
+ })
130
+
131
+
132
+
133
+ }}
134
+ >
135
+
136
+ <Icon source={"dots-vertical"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={Theme[themeTypeContext].icon_color}/>
137
+ </TouchableRipple>
138
+ </View>
139
+ </View>)
140
+ }</>
141
+ <>{manageBookmark.edit &&
142
+ (<View
143
+ style={{
144
+ display:"flex",
145
+ flexDirection:"row",
146
+ justifyContent:"space-between",
147
+ alignItems:"center",
148
+ width:"100%",
149
+ height:"auto",
150
+ gap:12,
151
+ padding:12,
152
+ }}
153
+ >
154
+ <View
155
+ style={{flex:1}}
156
+ >
157
+ <TextInput mode="outlined" label="Edit" textColor={Theme[themeTypeContext].text_color} maxLength={72}
158
+ right={<TextInput.Affix text={`| Max: 72`} />}
159
+ style={{
160
+ width:"100%",
161
+ height:"100%",
162
+ backgroundColor:Theme[themeTypeContext].background_color,
163
+ borderColor:Theme[themeTypeContext].border_color,
164
+
165
+ }}
166
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
167
+ value={editTag}
168
+ onChange={(event)=>{
169
+ setEditTag(event.nativeEvent.text)
170
+ }}
171
+ />
172
+ </View>
173
+ <View
174
+ style={{
175
+ display:"flex",
176
+ flexDirection:"row",
177
+ gap:8,
178
+ }}
179
+ >
180
+ <TouchableRipple
181
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
182
+ style={{
183
+ borderRadius:5,
184
+ borderWidth:0,
185
+ backgroundColor: "transparent",
186
+ padding:5,
187
+ }}
188
+
189
+ onPress={()=>{
190
+ setManageBookmark({...manageBookmark,edit:""})
191
+ setEditTag("")
192
+ setShowMenuOption({...showMenuOption,state:false,id:""})
193
+ }}
194
+ >
195
+
196
+ <Icon source={"close"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"red"}/>
197
+ </TouchableRipple>
198
+ <TouchableRipple
199
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
200
+ style={{
201
+ borderRadius:5,
202
+ borderWidth:0,
203
+ backgroundColor: "transparent",
204
+ padding:5,
205
+ }}
206
+
207
+ onPress={async ()=>{
208
+ const stored_bookmark = await Storage.get("bookmark");
209
+
210
+ const index = stored_bookmark.findIndex((item:string) => item === manageBookmark.edit);
211
+
212
+ if (index !== -1){
213
+ stored_bookmark[index] = editTag;
214
+ await Storage.store("bookmark", stored_bookmark)
215
+
216
+ const stored_comics:any = await ComicStorage.getByTag(manageBookmark.edit)
217
+ for (const item of stored_comics){
218
+ await ComicStorage.replaceTag(item.source, item.id, editTag)
219
+ }
220
+ if (manageBookmark.edit === defaultTag) {
221
+ onRefresh();
222
+ setWidgetContext({state:false,component:<></>});
223
+
224
+ }else{
225
+
226
+ const index = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.edit);
227
+ if (index !== -1){
228
+ BOOKMARK_DATA[index].label = editTag
229
+ BOOKMARK_DATA[index].value = editTag
230
+ }
231
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
232
+ setManageBookmark({...manageBookmark,edit:""})
233
+ setEditTag("")
234
+ }
235
+
236
+ }
237
+ setShowMenuOption({...showMenuOption,state:false,id:""})
238
+ }}
239
+ >
240
+
241
+ <Icon source={"check"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"green"}/>
242
+ </TouchableRipple>
243
+ </View>
244
+
245
+
246
+
247
+ </View>)
248
+
249
+ }</>
250
+
251
+
252
+
253
+
254
+ </View>
255
+
256
+ )
257
+ }
258
+ </>)
259
+ }
260
+
261
+
262
+
263
+ const load_bookmark = async ()=>{
264
+ const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
265
+
266
+ if (stored_comic) {
267
+ setDefaultTag(stored_comic.tag)
268
+ setBookmark(stored_comic.tag)
269
+ }
270
+
271
+ const stored_bookmark_data = await Storage.get("bookmark") || []
272
+ if (stored_bookmark_data.length) {
273
+ const bookmark_data:Array<Object> = []
274
+ for (const item of stored_bookmark_data) {
275
+ bookmark_data.push({
276
+ label:item,
277
+ value:item,
278
+ })
279
+ }
280
+
281
+ SET_BOOKMARK_DATA(bookmark_data.sort())
282
+ }else SET_BOOKMARK_DATA([])
283
+ }
284
+
285
+ useEffect(()=>{
286
+ console.log(BOOKMARK_DATA)
287
+ SET_MIGRATE_BOOKMARK_DATA([{label:"None",value:""},...BOOKMARK_DATA])
288
+ },[BOOKMARK_DATA])
289
+
290
+ useEffect(()=>{
291
+ load_bookmark()
292
+ return () => controller.abort();
293
+ },[])
294
+
295
+ return (<>{BOOKMARK_DATA !== null && <>
296
+
297
+ <View key={"BookmarkWidget"}
298
+ style={{
299
+ zIndex:10,
300
+ backgroundColor:Theme[themeTypeContext].background_color,
301
+ width:Dimensions.width*0.35,
302
+ minWidth:500,
303
+
304
+ borderColor:Theme[themeTypeContext].border_color,
305
+ borderWidth:2,
306
+ borderRadius:8,
307
+ padding:12,
308
+ display:"flex",
309
+ justifyContent:"center",
310
+
311
+ flexDirection:"column",
312
+ gap:12,
313
+ }}
314
+ from={{
315
+ opacity: 0,
316
+ scale: 0.9,
317
+ }}
318
+ animate={{
319
+ opacity: 1,
320
+ scale: 1,
321
+ }}
322
+ exit={{
323
+ opacity: 0,
324
+ scale: 0.5,
325
+ }}
326
+ transition={{
327
+ type: 'timing',
328
+ duration: 500,
329
+ }}
330
+ exitTransition={{
331
+ type: 'timing',
332
+ duration: 250,
333
+ }}
334
+ >
335
+
336
+ <>{!manageBookmark.state && !createTag.state && !removeTag.state &&
337
+ <>
338
+ <View
339
+ style={{
340
+ width:"100%",
341
+ height:"auto",
342
+ display:"flex",
343
+ flexDirection:"row",
344
+ alignItems:"flex-end",
345
+ justifyContent:"space-between",
346
+ gap:8,
347
+ }}
348
+ >
349
+ <View style={{flex:1}}>
350
+ <Dropdown
351
+ theme_type={themeTypeContext}
352
+ Dimensions={Dimensions}
353
+
354
+ label='Add to bookmark'
355
+ data={BOOKMARK_DATA}
356
+ value={bookmark}
357
+ onChange={(async (item:any) => {
358
+ setBookmark(item.value)
359
+ })}
360
+ />
361
+ </View>
362
+ <>{bookmark &&
363
+ <TouchableRipple
364
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
365
+ style={{
366
+ padding:5,
367
+ borderRadius:5,
368
+ borderWidth:0,
369
+ backgroundColor: "transparent",
370
+ }}
371
+ onPress={(async ()=>{
372
+ const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
373
+ if (stored_comic) setRemoveTag({...removeTag,state:true})
374
+ else setBookmark("")
375
+ })}
376
+ >
377
+ <Icon source={"tag-remove-outline"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"red"}/>
378
+ </TouchableRipple>
379
+ }</>
380
+ </View>
381
+
382
+
383
+ <View
384
+ style={{
385
+ display:"flex",
386
+ flexDirection:"row",
387
+ width:"100%",
388
+ justifyContent:"space-around",
389
+ alignItems:"center",
390
+ }}
391
+ >
392
+ <Button mode='contained'
393
+ labelStyle={{
394
+ color:Theme[themeTypeContext].text_color,
395
+ fontFamily:"roboto-medium",
396
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
397
+ paddingVertical:4,
398
+ }}
399
+ style={{backgroundColor:"blue",borderRadius:5}}
400
+ onPress={(()=>{
401
+ setManageBookmark({...manageBookmark,state:true})
402
+ })}
403
+ >Manage Bookmark</Button>
404
+ <Button mode='outlined'
405
+ labelStyle={{
406
+ color:Theme[themeTypeContext].text_color,
407
+ fontFamily:"roboto-medium",
408
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
409
+
410
+
411
+ }}
412
+ style={{
413
+
414
+ borderRadius:5,
415
+ borderWidth:2,
416
+ borderColor:Theme[themeTypeContext].border_color
417
+ }}
418
+ onPress={(async ()=>{
419
+ if (defaultTag !== bookmark){
420
+ const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
421
+ if (stored_comic) await ComicStorage.replaceTag(SOURCE, CONTENT.id, bookmark)
422
+ else {
423
+ const cover_result:any = await store_comic_cover(setShowCloudflareTurnstileContext,signal,SOURCE,ID,CONTENT)
424
+
425
+ await ComicStorage.store(SOURCE,CONTENT.id, bookmark, {
426
+ cover:cover_result,
427
+ title:CONTENT.title,
428
+ author:CONTENT.author,
429
+ category:CONTENT.category,
430
+ status:CONTENT.status,
431
+ synopsis:CONTENT.synopsis,
432
+ updated:CONTENT.updated,
433
+ })
434
+ }
435
+ onRefresh()
436
+ }
437
+ setWidgetContext({state:false,component:<></>})
438
+
439
+ })}
440
+ >Done</Button>
441
+ </View>
442
+ </>
443
+ }</>
444
+
445
+ <>{manageBookmark.state && !createTag.state && !removeTag.state && <>
446
+ <View
447
+
448
+ style={{
449
+ display:"flex",
450
+ flexDirection:"column",
451
+ gap:18,
452
+ }}
453
+ >
454
+ <>{BOOKMARK_DATA.length
455
+ ? <>
456
+ <View
457
+ style={{flex:1}}
458
+ >
459
+ <TextInput mode="outlined" label="Search" textColor={Theme[themeTypeContext].text_color}
460
+ placeholder={""}
461
+ style={{
462
+
463
+ backgroundColor:Theme[themeTypeContext].background_color,
464
+ borderColor:Theme[themeTypeContext].border_color,
465
+
466
+ }}
467
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
468
+ value={searchTag}
469
+ onChange={(event)=>{
470
+ setSearchTag(event.nativeEvent.text)
471
+ }}
472
+ />
473
+ </View>
474
+ <View
475
+ style={{
476
+ maxHeight:Dimensions.height*0.7
477
+ }}
478
+ >
479
+ <ScrollView
480
+ contentContainerStyle={{
481
+ display:"flex",
482
+ flexDirection:"column",
483
+ justifyContent:"space-around",
484
+ gap:8,
485
+
486
+ height:"auto",
487
+ paddingVertical:12,
488
+ paddingHorizontal:8,
489
+ }}
490
+ style={{
491
+
492
+ }}
493
+ >
494
+ <>{BOOKMARK_DATA.map((item:any) => <Fragment key={item.value}>
495
+ <RenderTag item={item}/>
496
+ <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].border_color}}/>
497
+ </Fragment>)}</>
498
+ </ScrollView>
499
+ </View>
500
+ </>
501
+ : <>
502
+ <Text style={{
503
+ width:"100%",
504
+ textAlign:"center",
505
+ color:Theme[themeTypeContext].text_color,
506
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.045,
507
+ fontFamily:"roboto-bold",
508
+ }}>No bookmark found</Text>
509
+ </>
510
+
511
+ }</>
512
+
513
+ <View
514
+ style={{
515
+ display:"flex",
516
+ flexDirection:"row",
517
+ width:"100%",
518
+ justifyContent:"space-around",
519
+ alignItems:"center",
520
+ }}
521
+ >
522
+ <Button mode='contained'
523
+ labelStyle={{
524
+ color:Theme[themeTypeContext].text_color,
525
+ fontFamily:"roboto-medium",
526
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
527
+ }}
528
+ style={{backgroundColor:"green",borderRadius:5}}
529
+ onPress={(()=>{
530
+ setCreateTag({state:true,title:""})
531
+ setShowMenuOption({...showMenuOption,state:false,id:""})
532
+ })}
533
+ >+ Create Bookmark</Button>
534
+ <Button mode='outlined'
535
+ labelStyle={{
536
+ color:Theme[themeTypeContext].text_color,
537
+ fontFamily:"roboto-medium",
538
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
539
+
540
+
541
+ }}
542
+ style={{
543
+
544
+ borderRadius:5,
545
+ borderWidth:2,
546
+ borderColor:Theme[themeTypeContext].border_color
547
+ }}
548
+ onPress={(async ()=>{
549
+ setManageBookmark({...manageBookmark,state:false,edit:"",delete:""})
550
+ setShowMenuOption({...showMenuOption,state:false,id:""})
551
+ })}
552
+ >Back</Button>
553
+ </View>
554
+ </View>
555
+ </>}</>
556
+
557
+ <>{createTag.state &&
558
+ <>
559
+ <View
560
+ style={{
561
+ height:"auto",
562
+ display:"flex",
563
+ flexDirection:"column",
564
+ gap:12,
565
+ }}
566
+ >
567
+ <TextInput mode="outlined" label="Create Bookmark" textColor={Theme[themeTypeContext].text_color} maxLength={72}
568
+ placeholder="Bookmark Tag"
569
+
570
+ right={<TextInput.Affix text={`| Max: 72`} />}
571
+ style={{
572
+
573
+ backgroundColor:Theme[themeTypeContext].background_color,
574
+ borderColor:Theme[themeTypeContext].border_color,
575
+
576
+ }}
577
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
578
+ value={createTag.title}
579
+ onChange={(event)=>{
580
+ setCreateTag({...createTag,title:event.nativeEvent.text})
581
+ }}
582
+ />
583
+ </View>
584
+ <View
585
+ style={{
586
+ display:"flex",
587
+ flexDirection:"row",
588
+ width:"100%",
589
+ justifyContent:"space-around",
590
+ alignItems:"center",
591
+ }}
592
+ >
593
+ <Button mode='outlined'
594
+ labelStyle={{
595
+ color:Theme[themeTypeContext].text_color,
596
+ fontFamily:"roboto-medium",
597
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
598
+
599
+
600
+ }}
601
+ style={{
602
+
603
+ borderRadius:5,
604
+ borderWidth:2,
605
+ borderColor:Theme[themeTypeContext].border_color
606
+ }}
607
+ onPress={(()=>{
608
+
609
+ setCreateTag({...createTag,state:false})
610
+
611
+ })}
612
+ >Cancel</Button>
613
+ <Button mode='contained'
614
+ labelStyle={{
615
+ color:Theme[themeTypeContext].text_color,
616
+ fontFamily:"roboto-medium",
617
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
618
+ }}
619
+ style={{backgroundColor:"green",borderRadius:5}}
620
+ onPress={(async()=>{
621
+
622
+ const title = createTag.title
623
+ if (!title) return
624
+
625
+ const stored_bookmark_data = await Storage.get("bookmark") || []
626
+ if (stored_bookmark_data.includes(title)){
627
+ Toast.show({
628
+ type: 'error',
629
+ text1: 'πŸ”– Duplicate Bookmark',
630
+ text2: `Tag "${title}" already existed in your bookmark.`,
631
+
632
+ position: "bottom",
633
+ visibilityTime: 5000,
634
+ text1Style:{
635
+ fontFamily:"roboto-bold",
636
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
637
+ },
638
+ text2Style:{
639
+ fontFamily:"roboto-medium",
640
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
641
+
642
+ },
643
+ });
644
+ }else{
645
+ await Storage.store("bookmark", [...stored_bookmark_data,title].sort())
646
+ SET_BOOKMARK_DATA([...BOOKMARK_DATA,
647
+ {label:title,value:title}
648
+ ].sort())
649
+ setCreateTag({state:false,title:""})
650
+ Toast.show({
651
+ type: 'info',
652
+ text1: 'πŸ”– Create Bookmark',
653
+ text2: `Tag "${title}" added to your bookmark.`,
654
+
655
+ position: "bottom",
656
+ visibilityTime: 3000,
657
+ text1Style:{
658
+ fontFamily:"roboto-bold",
659
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
660
+ },
661
+ text2Style:{
662
+ fontFamily:"roboto-medium",
663
+ fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
664
+
665
+ },
666
+ });
667
+ }
668
+ })}
669
+ >Add</Button>
670
+ </View>
671
+ </>
672
+ }</>
673
+ <>{removeTag.state &&
674
+ <>
675
+ <View
676
+ style={{
677
+ height:"auto",
678
+ display:"flex",
679
+ flexDirection:"column",
680
+ gap:12,
681
+ }}
682
+ >
683
+ <Text
684
+ style={{
685
+ color:Theme[themeTypeContext].text_color,
686
+ fontFamily:"roboto-bold",
687
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03,
688
+ textAlign:"center",
689
+ }}
690
+ >Are you sure you want to remove this comic from bookmark?</Text>
691
+
692
+ <Text
693
+ style={{
694
+ color:Theme[themeTypeContext].text_color,
695
+ fontFamily:"roboto-medium",
696
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.025,
697
+ textAlign:"center",
698
+ }}
699
+ >This will remove all local saved info and chapters.</Text>
700
+ </View>
701
+
702
+ <View
703
+ style={{
704
+ display:"flex",
705
+ flexDirection:"row",
706
+ width:"100%",
707
+ justifyContent:"space-around",
708
+ alignItems:"center",
709
+ }}
710
+ >
711
+ <>{removeTag.removing
712
+ ? <ActivityIndicator animating={true}/>
713
+ :<>
714
+
715
+ <Button mode='outlined' disabled={removeTag.removing}
716
+ labelStyle={{
717
+ color:Theme[themeTypeContext].text_color,
718
+ fontFamily:"roboto-medium",
719
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
720
+
721
+
722
+ }}
723
+ style={{
724
+
725
+ borderRadius:5,
726
+ borderWidth:2,
727
+ borderColor:Theme[themeTypeContext].border_color
728
+ }}
729
+ onPress={(()=>{
730
+ setRemoveTag({...removeTag,state:false})
731
+ })}
732
+ >No</Button>
733
+ <Button mode='contained' disabled={removeTag.removing}
734
+ labelStyle={{
735
+ color:Theme[themeTypeContext].text_color,
736
+ fontFamily:"roboto-medium",
737
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
738
+ }}
739
+ style={{backgroundColor:"red",borderRadius:5}}
740
+ onPress={(async ()=>{
741
+ setRemoveTag({...removeTag,removing:false})
742
+ if (Platform.OS !== "web"){
743
+ const comic_dir = FileSystem.documentDirectory + "ComicMTL/" + `${SOURCE}/` + `${ID}/`
744
+ await FileSystem.deleteAsync(comic_dir, { idempotent: true })
745
+ }
746
+
747
+ await ChapterStorage.drop(`${SOURCE}-${CONTENT.id}`)
748
+ await ComicStorage.removeByID(SOURCE,CONTENT.id)
749
+
750
+ onRefresh()
751
+ setWidgetContext({state:false,component:<></>})
752
+ })}
753
+ >Yes</Button>
754
+ </>
755
+ }</>
756
+
757
+ </View>
758
+ </>
759
+ }</>
760
+
761
+ </View>
762
+ <>{showMenuOption.state && manageBookmark.state &&
763
+ <View
764
+ style={{
765
+ display:"flex",
766
+ position:"absolute",
767
+ zIndex:11,
768
+ justifyContent:"space-around",
769
+ flexDirection:"column",
770
+
771
+ backgroundColor:Theme[themeTypeContext].border_color,
772
+ top:showMenuOption.positions[0],
773
+ bottom:showMenuOption.positions[1],
774
+ left:showMenuOption.positions[2],
775
+ right:showMenuOption.positions[3],
776
+
777
+ width:(Dimensions.width+Dimensions.height)/2*0.2,
778
+ height:(Dimensions.width+Dimensions.height)/2*0.1,
779
+
780
+ borderRadius:5,
781
+ borderWidth:2,
782
+ borderColor:Theme[themeTypeContext].background_color
783
+ }}
784
+ >
785
+ <TouchableRipple
786
+
787
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
788
+ style={{
789
+
790
+ borderWidth:0,
791
+ backgroundColor: "transparent",
792
+ padding:5,
793
+ width:"100%",
794
+
795
+ }}
796
+
797
+ onPress={(event)=>{
798
+ setManageBookmark({...manageBookmark,edit:showMenuOption.id})
799
+ setShowMenuOption({...showMenuOption,state:false})
800
+ }}
801
+ >
802
+ <View
803
+ style={{
804
+ display:"flex",
805
+ flexDirection:"row",
806
+ justifyContent:"center",
807
+ alignItems:"center",
808
+ paddingHorizontal:18,
809
+
810
+ }}
811
+ >
812
+ <Icon source={"pencil"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"blue"}/>
813
+ <View>
814
+ <Text selectable={false}
815
+ style={{
816
+ textAlign:"center",
817
+ color:"blue",
818
+ fontFamily:"roboto-medium",
819
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
820
+ }}
821
+ >Edit</Text>
822
+ </View>
823
+ </View>
824
+ </TouchableRipple>
825
+ <View style={{width:"100%",height:2,backgroundColor:Theme[themeTypeContext].background_color}}/>
826
+ <TouchableRipple
827
+
828
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
829
+ style={{
830
+
831
+ borderWidth:0,
832
+ backgroundColor: "transparent",
833
+ padding:5,
834
+ width:"100%",
835
+ }}
836
+
837
+ onPress={(event)=>{
838
+ setManageBookmark({...manageBookmark,edit:"",delete:showMenuOption.id})
839
+ setShowMenuOption({...showMenuOption,state:false})
840
+ }}
841
+ >
842
+ <View
843
+ style={{
844
+ display:"flex",
845
+ flexDirection:"row",
846
+ justifyContent:"center",
847
+ alignItems:"center",
848
+ paddingHorizontal:18,
849
+
850
+ }}
851
+ >
852
+ <Icon source={"trash-can"} size={((Dimensions.width+Dimensions.height)/2)*0.025} color={"red"}/>
853
+ <View>
854
+ <Text selectable={false}
855
+ style={{
856
+ textAlign:"center",
857
+ color:"red",
858
+ fontFamily:"roboto-medium",
859
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
860
+ }}
861
+ >Delete</Text>
862
+ </View>
863
+ </View>
864
+ </TouchableRipple>
865
+
866
+ </View>
867
+
868
+ }</>
869
+ <>{manageBookmark.delete && (
870
+
871
+
872
+ <View
873
+ style={{
874
+ top:0,
875
+ left:0,
876
+ position:"absolute",
877
+ width:Dimensions.width,
878
+ height:Dimensions.height,
879
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
880
+ zIndex:11,
881
+ display:"flex",
882
+ justifyContent:"center",
883
+ alignItems:"center",
884
+ }}
885
+ >
886
+ <View
887
+ style={{
888
+ backgroundColor:Theme[themeTypeContext].background_color,
889
+ width:Dimensions.width*0.35,
890
+ minWidth:500,
891
+ height:"auto",
892
+
893
+ borderColor:Theme[themeTypeContext].border_color,
894
+ borderWidth:2,
895
+ borderRadius:8,
896
+ padding:12,
897
+ display:"flex",
898
+ justifyContent:"center",
899
+
900
+ flexDirection:"column",
901
+ gap:18,
902
+ }}
903
+ >
904
+ <View
905
+ style={{
906
+ borderBottomWidth:2,
907
+ borderColor:Theme[themeTypeContext].border_color,
908
+ padding:8,
909
+ width:"100%",
910
+ }}
911
+ >
912
+ <Text
913
+ numberOfLines={1}
914
+ style={{
915
+ color:"red",
916
+ fontFamily:"roboto-bold",
917
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.03,
918
+ textAlign:"center",
919
+ }}
920
+ >Delete Tag: "{manageBookmark.delete}"</Text>
921
+ </View>
922
+ <View
923
+ style={{
924
+ width:"100%",
925
+ display:"flex",
926
+ flexDirection:"column",
927
+ gap:12,
928
+ }}
929
+ >
930
+ <View style={{flex:1}}>
931
+ <Dropdown
932
+ theme_type={themeTypeContext}
933
+ Dimensions={Dimensions}
934
+
935
+ label='Migrate comics to tag'
936
+ data={MIGRATE_BOOKMARK_DATA.filter((item:any) => item.value !== manageBookmark.delete)}
937
+ value={migrateTag}
938
+ onChange={(async (item:any) => {
939
+ setMigrateTag(item.value)
940
+ })}
941
+ />
942
+ </View>
943
+ <>{!migrateTag && (
944
+ <Text
945
+ style={{
946
+ color:Theme[themeTypeContext].text_color,
947
+ fontFamily:"roboto-bold",
948
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
949
+ textAlign:"center",
950
+ }}
951
+ >Setting migration to None will remove all comics and chapters for this bookmark tag.</Text>
952
+ )}</>
953
+ <View
954
+ style={{
955
+ display:"flex",
956
+ flexDirection:"row",
957
+ width:"100%",
958
+ justifyContent:"space-around",
959
+ alignItems:"center",
960
+ }}
961
+ >
962
+ <>{migrateTag
963
+
964
+ ? <TouchableRipple
965
+
966
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
967
+ style={{
968
+
969
+ borderWidth:0,
970
+ backgroundColor: "blue",
971
+ padding:5,
972
+ borderRadius:8,
973
+ paddingHorizontal:12,
974
+ paddingVertical:8,
975
+
976
+
977
+ }}
978
+
979
+ onPress={async (event)=>{
980
+ const stored_bookmark = await Storage.get("bookmark")
981
+ const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete)
982
+ if (index === -1) return
983
+
984
+ const stored_comics = await ComicStorage.getByTag(manageBookmark.delete)
985
+ for (const comic of stored_comics) {
986
+ const source = comic.source;
987
+ const comic_id = comic.id
988
+ await ComicStorage.replaceTag(source,comic_id,migrateTag)
989
+
990
+ }
991
+
992
+ stored_bookmark.splice(index, 1);
993
+ await Storage.store("bookmark",stored_bookmark);
994
+
995
+ if (defaultTag === manageBookmark.delete) {
996
+ setWidgetContext({state:false,component:<></>});
997
+ onRefresh();
998
+ }else {
999
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete);
1000
+ if (index_2 !== -1){
1001
+ BOOKMARK_DATA.splice(index_2, 1);
1002
+ }
1003
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
1004
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
1005
+ setShowMenuOption({...showMenuOption,state:false,id:""})
1006
+ setMigrateTag("")
1007
+ }
1008
+
1009
+ }}
1010
+ >
1011
+
1012
+ <Text selectable={false}
1013
+ style={{
1014
+ textAlign:"center",
1015
+ color:Theme[themeTypeContext].text_color,
1016
+ fontFamily:"roboto-medium",
1017
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
1018
+ }}
1019
+ >Migrate</Text>
1020
+
1021
+ </TouchableRipple>
1022
+ : <TouchableRipple
1023
+
1024
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
1025
+ style={{
1026
+
1027
+ borderWidth:0,
1028
+ backgroundColor: "red",
1029
+ padding:5,
1030
+ borderRadius:8,
1031
+ paddingHorizontal:12,
1032
+ paddingVertical:8,
1033
+
1034
+
1035
+ }}
1036
+
1037
+ onPress={async (event)=>{
1038
+ const stored_bookmark = await Storage.get("bookmark");
1039
+ const index = stored_bookmark.findIndex((item:any) => item === manageBookmark.delete)
1040
+ if (index === -1) return
1041
+ await ComicStorage.removeByTag(manageBookmark.delete);
1042
+ stored_bookmark.splice(index, 1);
1043
+ await Storage.store("bookmark",stored_bookmark);
1044
+
1045
+ if (defaultTag === manageBookmark.delete) {
1046
+ setWidgetContext({state:false,component:<></>});
1047
+ onRefresh();
1048
+ }else {
1049
+ const index_2 = BOOKMARK_DATA.findIndex((item:any) => item.value === manageBookmark.delete);
1050
+ if (index_2 !== -1){
1051
+ BOOKMARK_DATA.splice(index_2, 1);
1052
+ }
1053
+ SET_BOOKMARK_DATA(BOOKMARK_DATA)
1054
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
1055
+ setShowMenuOption({...showMenuOption,state:false,id:""})
1056
+ setMigrateTag("")
1057
+ }
1058
+
1059
+ }}
1060
+ >
1061
+
1062
+ <Text selectable={false}
1063
+ style={{
1064
+ textAlign:"center",
1065
+ color:Theme[themeTypeContext].text_color,
1066
+ fontFamily:"roboto-medium",
1067
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
1068
+ }}
1069
+ >Delete</Text>
1070
+
1071
+ </TouchableRipple>
1072
+ }</>
1073
+ <TouchableRipple
1074
+
1075
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
1076
+ style={{
1077
+
1078
+ borderWidth:2,
1079
+ borderColor:Theme[themeTypeContext].border_color,
1080
+ backgroundColor: "transparent",
1081
+ padding:5,
1082
+ borderRadius:8,
1083
+ paddingHorizontal:12,
1084
+ paddingVertical:8,
1085
+
1086
+
1087
+ }}
1088
+
1089
+ onPress={(event)=>{
1090
+ setShowMenuOption({...showMenuOption,state:false,id:""})
1091
+ setManageBookmark({...manageBookmark,edit:"",delete:""})
1092
+ }}
1093
+ >
1094
+
1095
+ <Text selectable={false}
1096
+ style={{
1097
+ textAlign:"center",
1098
+ color:Theme[themeTypeContext].text_color,
1099
+ fontFamily:"roboto-medium",
1100
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
1101
+ }}
1102
+ >Cancel</Text>
1103
+
1104
+ </TouchableRipple>
1105
+ </View>
1106
+
1107
+ </View>
1108
+ </View>
1109
+
1110
+ </View>
1111
+ )}</>
1112
+
1113
+ </>}</>)
1114
+ }
1115
+
1116
+ export default BookmarkWidget;
frontend/app/view/componenets/widgets/page_navigation.tsx ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useEffect, useState, useCallback, useContext, useRef, Fragment } from 'react';
3
+ import { Platform, useWindowDimensions, ScrollView } from 'react-native';
4
+
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper';
6
+ import { View, AnimatePresence } from 'moti';
7
+ import Toast from 'react-native-toast-message';
8
+ import * as FileSystem from 'expo-file-system';
9
+ import axios from 'axios';
10
+
11
+
12
+ import Theme from '@/constants/theme';
13
+ import Dropdown from '@/components/dropdown';
14
+ import { CONTEXT } from '@/constants/module/context';
15
+ import { store_comic_cover } from '../../modules/content';
16
+ import Storage from '@/constants/module/storages/storage';
17
+ import ComicStorage from '@/constants/module/storages/comic_storage';
18
+ import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
19
+ import ChapterStorage from '@/constants/module/storages/chapter_storage';
20
+
21
+
22
+
23
+ const PageNavigationWidget = ({MAX_OFFSET,setPage,CONTENT}:any) =>{
24
+ const Dimensions = useWindowDimensions();
25
+
26
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
27
+ const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
28
+
29
+ const [goToPage, setGoToPage] = useState("");
30
+ const [_feedBack, _setFeedBack] = useState("");
31
+ return (<View
32
+ style={{
33
+ backgroundColor:Theme[themeTypeContext].background_color,
34
+ maxWidth:500,
35
+ width:"100%",
36
+
37
+ borderColor:Theme[themeTypeContext].border_color,
38
+ borderWidth:2,
39
+ borderRadius:8,
40
+ padding:12,
41
+ display:"flex",
42
+ justifyContent:"center",
43
+
44
+ flexDirection:"column",
45
+ gap:12,
46
+ }}
47
+ from={{
48
+ opacity: 0,
49
+ scale: 0.9,
50
+ }}
51
+ animate={{
52
+ opacity: 1,
53
+ scale: 1,
54
+ }}
55
+ exit={{
56
+ opacity: 0,
57
+ scale: 0.5,
58
+ }}
59
+ transition={{
60
+ type: 'timing',
61
+ duration: 500,
62
+ }}
63
+ exitTransition={{
64
+ type: 'timing',
65
+ duration: 250,
66
+ }}
67
+ >
68
+ <View style={{height:"auto"}}>
69
+ <TextInput mode="outlined" label="Go to page" textColor={Theme[themeTypeContext].text_color} maxLength={1000000000}
70
+ placeholder="Go to page"
71
+ right={<TextInput.Affix text={`/${Math.ceil(CONTENT.chapters.length/MAX_OFFSET)}`} />}
72
+ style={{
73
+
74
+ backgroundColor:Theme[themeTypeContext].background_color,
75
+ borderColor:Theme[themeTypeContext].border_color,
76
+
77
+ }}
78
+ outlineColor={Theme[themeTypeContext].text_input_border_color}
79
+ value={goToPage}
80
+ onChange={(event)=>{
81
+
82
+ const value = event.nativeEvent.text
83
+
84
+ const isInt = /^-?\d+$/.test(value);
85
+ if (isInt || value === "") {
86
+ if (parseInt(value) > Math.ceil(CONTENT.chapters.length/MAX_OFFSET)){
87
+ _setFeedBack("Page is out of index.")
88
+ }else{
89
+ _setFeedBack("")
90
+ setGoToPage(value)
91
+ }
92
+
93
+ }
94
+ else _setFeedBack("Input is not a valid number.")
95
+
96
+ }}
97
+ />
98
+
99
+ </View>
100
+ {_feedBack
101
+ ? <Text
102
+ style={{
103
+ color:Theme[themeTypeContext].text_color,
104
+ fontFamily:"roboto-medium",
105
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
106
+ textAlign:"center",
107
+ }}
108
+
109
+ >{_feedBack}</Text>
110
+ : <></>
111
+ }
112
+ <View
113
+ style={{
114
+ display:"flex",
115
+ flexDirection:"row",
116
+ width:"100%",
117
+ justifyContent:"space-around",
118
+ alignItems:"center",
119
+ }}
120
+ >
121
+ <Button mode='contained'
122
+ labelStyle={{
123
+ color:Theme[themeTypeContext].text_color,
124
+ fontFamily:"roboto-medium",
125
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
126
+ }}
127
+ style={{backgroundColor:"red",borderRadius:5}}
128
+ onPress={(()=>{
129
+
130
+ setWidgetContext({state:false,component:<></>})
131
+
132
+ })}
133
+ >Cancel</Button>
134
+ <Button mode='contained'
135
+ labelStyle={{
136
+ color:Theme[themeTypeContext].text_color,
137
+ fontFamily:"roboto-medium",
138
+ fontSize:(Dimensions.width+Dimensions.height)/2*0.02
139
+ }}
140
+ style={{backgroundColor:"green",borderRadius:5}}
141
+ onPress={(()=>{
142
+ const isInt = /^-?\d+$/.test(goToPage);
143
+ if (isInt) {
144
+ if (parseInt(goToPage) > Math.ceil(CONTENT.chapters.length/MAX_OFFSET) || !parseInt(goToPage)){
145
+ _setFeedBack("Page is out of index.")
146
+ }else{
147
+ setPage(parseInt(goToPage))
148
+ setWidgetContext({state:false,component:<></>})
149
+ }
150
+
151
+ }else _setFeedBack("Input is not a valid number.")
152
+ })}
153
+ >Go</Button>
154
+ </View>
155
+
156
+ </View>)
157
+ }
158
+
159
+ export default PageNavigationWidget;
frontend/app/view/componenets/{widgets.tsx β†’ widgets/request_chapter.tsx} RENAMED
@@ -1,7 +1,8 @@
1
- import React, { useEffect, useState, useCallback, useContext, useRef } from 'react';
2
- import { Platform, useWindowDimensions } from 'react-native';
3
 
4
- import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator } from 'react-native-paper';
 
 
 
5
  import { View, AnimatePresence } from 'moti';
6
  import Toast from 'react-native-toast-message';
7
  import * as FileSystem from 'expo-file-system';
@@ -11,149 +12,13 @@ import axios from 'axios';
11
  import Theme from '@/constants/theme';
12
  import Dropdown from '@/components/dropdown';
13
  import { CONTEXT } from '@/constants/module/context';
14
- import { store_comic_cover } from '../modules/content';
15
  import Storage from '@/constants/module/storages/storage';
16
  import ComicStorage from '@/constants/module/storages/comic_storage';
17
  import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
18
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
19
 
20
 
21
- export const PageNavigationWidget = ({MAX_OFFSET,setPage,CONTENT}:any) =>{
22
- const Dimensions = useWindowDimensions();
23
-
24
- const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
25
- const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
26
-
27
- const [goToPage, setGoToPage] = useState("");
28
- const [_feedBack, _setFeedBack] = useState("");
29
- return (<View
30
- style={{
31
- backgroundColor:Theme[themeTypeContext].background_color,
32
- maxWidth:500,
33
- width:"100%",
34
-
35
- borderColor:Theme[themeTypeContext].border_color,
36
- borderWidth:2,
37
- borderRadius:8,
38
- padding:12,
39
- display:"flex",
40
- justifyContent:"center",
41
-
42
- flexDirection:"column",
43
- gap:12,
44
- }}
45
- from={{
46
- opacity: 0,
47
- scale: 0.9,
48
- }}
49
- animate={{
50
- opacity: 1,
51
- scale: 1,
52
- }}
53
- exit={{
54
- opacity: 0,
55
- scale: 0.5,
56
- }}
57
- transition={{
58
- type: 'timing',
59
- duration: 500,
60
- }}
61
- exitTransition={{
62
- type: 'timing',
63
- duration: 250,
64
- }}
65
- >
66
- <View style={{height:"auto"}}>
67
- <TextInput mode="outlined" label="Go to page" textColor={Theme[themeTypeContext].text_color} maxLength={1000000000}
68
- placeholder="Go to page"
69
- right={<TextInput.Affix text={`/${Math.ceil(CONTENT.chapters.length/MAX_OFFSET)}`} />}
70
- style={{
71
-
72
- backgroundColor:Theme[themeTypeContext].background_color,
73
- borderColor:Theme[themeTypeContext].border_color,
74
-
75
- }}
76
- outlineColor={Theme[themeTypeContext].text_input_border_color}
77
- value={goToPage}
78
- onChange={(event)=>{
79
-
80
- const value = event.nativeEvent.text
81
-
82
- const isInt = /^-?\d+$/.test(value);
83
- if (isInt || value === "") {
84
- if (parseInt(value) > Math.ceil(CONTENT.chapters.length/MAX_OFFSET)){
85
- _setFeedBack("Page is out of index.")
86
- }else{
87
- _setFeedBack("")
88
- setGoToPage(value)
89
- }
90
-
91
- }
92
- else _setFeedBack("Input is not a valid number.")
93
-
94
- }}
95
- />
96
-
97
- </View>
98
- {_feedBack
99
- ? <Text
100
- style={{
101
- color:Theme[themeTypeContext].text_color,
102
- fontFamily:"roboto-medium",
103
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
104
- textAlign:"center",
105
- }}
106
-
107
- >{_feedBack}</Text>
108
- : <></>
109
- }
110
- <View
111
- style={{
112
- display:"flex",
113
- flexDirection:"row",
114
- width:"100%",
115
- justifyContent:"space-around",
116
- alignItems:"center",
117
- }}
118
- >
119
- <Button mode='contained'
120
- labelStyle={{
121
- color:Theme[themeTypeContext].text_color,
122
- fontFamily:"roboto-medium",
123
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02
124
- }}
125
- style={{backgroundColor:"red",borderRadius:5}}
126
- onPress={(()=>{
127
-
128
- setWidgetContext({state:false,component:<></>})
129
-
130
- })}
131
- >Cancel</Button>
132
- <Button mode='contained'
133
- labelStyle={{
134
- color:Theme[themeTypeContext].text_color,
135
- fontFamily:"roboto-medium",
136
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02
137
- }}
138
- style={{backgroundColor:"green",borderRadius:5}}
139
- onPress={(()=>{
140
- const isInt = /^-?\d+$/.test(goToPage);
141
- if (isInt) {
142
- if (parseInt(goToPage) > Math.ceil(CONTENT.chapters.length/MAX_OFFSET) || !parseInt(goToPage)){
143
- _setFeedBack("Page is out of index.")
144
- }else{
145
- setPage(parseInt(goToPage))
146
- setWidgetContext({state:false,component:<></>})
147
- }
148
-
149
- }else _setFeedBack("Input is not a valid number.")
150
- })}
151
- >Go</Button>
152
- </View>
153
-
154
- </View>)
155
- }
156
-
157
  interface RequestChapterWidgetProps {
158
  SOURCE: string | string[];
159
  ID: string | string[];
@@ -165,7 +30,7 @@ interface RequestChapterWidgetProps {
165
  get_requested_info: any;
166
  }
167
 
168
- export const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
169
  SOURCE,
170
  ID,
171
  CHAPTER,
@@ -453,413 +318,4 @@ export const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
453
  </View>)
454
  }
455
 
456
- interface BookmarkWidgetProps {
457
- onRefresh: any;
458
- SOURCE: string | string[];
459
- ID: string | string[];
460
- CONTENT: any;
461
- }
462
-
463
- export const BookmarkWidget: React.FC<BookmarkWidgetProps> = ({
464
- onRefresh,
465
- SOURCE,
466
- ID,
467
- CONTENT
468
- }) => {
469
- const Dimensions = useWindowDimensions();
470
-
471
- const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
472
- const {widgetContext, setWidgetContext}:any = useContext(CONTEXT)
473
- const {showCloudflareTurnstileContext, setShowCloudflareTurnstileContext}:any = useContext(CONTEXT)
474
- const {apiBaseContext, setApiBaseContext}:any = useContext(CONTEXT)
475
-
476
- const [BOOKMARK_DATA, SET_BOOKMARK_DATA]: any = useState(null)
477
-
478
- const [defaultBookmark, setDefaultBookmark]:any = useState("")
479
- const [bookmark, setBookmark]:any = useState("")
480
- const [createBookmark, setCreateBookmark]:any = useState({state:false,title:""})
481
- const [removeBookmark, setRemoveBookmark]:any = useState({state:false, removing: false})
482
-
483
- const controller = new AbortController();
484
- const signal = controller.signal;
485
-
486
-
487
- useEffect(()=>{
488
- (async ()=>{
489
- const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
490
-
491
- if (stored_comic) {
492
- setDefaultBookmark(stored_comic.tag)
493
- setBookmark(stored_comic.tag)
494
- }
495
-
496
- const stored_bookmark_data = await Storage.get("bookmark") || []
497
- if (stored_bookmark_data.length) {
498
- const bookmark_data:Array<Object> = []
499
- for (const item of stored_bookmark_data) {
500
- bookmark_data.push({
501
- label:item,
502
- value:item,
503
- })
504
- }
505
-
506
- SET_BOOKMARK_DATA(bookmark_data.sort())
507
- }else SET_BOOKMARK_DATA([])
508
- })()
509
- return () => controller.abort();
510
- },[])
511
-
512
- return (<>{BOOKMARK_DATA !== null &&
513
-
514
- <View key={"BookmarkWidget"}
515
- style={{
516
- backgroundColor:Theme[themeTypeContext].background_color,
517
- maxWidth:500,
518
- width:"100%",
519
-
520
- borderColor:Theme[themeTypeContext].border_color,
521
- borderWidth:2,
522
- borderRadius:8,
523
- padding:12,
524
- display:"flex",
525
- justifyContent:"center",
526
-
527
- flexDirection:"column",
528
- gap:12,
529
- }}
530
- from={{
531
- opacity: 0,
532
- scale: 0.9,
533
- }}
534
- animate={{
535
- opacity: 1,
536
- scale: 1,
537
- }}
538
- exit={{
539
- opacity: 0,
540
- scale: 0.5,
541
- }}
542
- transition={{
543
- type: 'timing',
544
- duration: 500,
545
- }}
546
- exitTransition={{
547
- type: 'timing',
548
- duration: 250,
549
- }}
550
- >
551
-
552
- <>{!createBookmark.state && !removeBookmark.state &&
553
- <>
554
- <View
555
- style={{
556
- width:"100%",
557
- height:"auto",
558
- display:"flex",
559
- flexDirection:"row",
560
- alignItems:"flex-end",
561
- justifyContent:"space-between",
562
- gap:8,
563
- }}
564
- >
565
- <View style={{flex:1}}>
566
- <Dropdown
567
- theme_type={themeTypeContext}
568
- Dimensions={Dimensions}
569
-
570
- label='Add to bookmark'
571
- data={BOOKMARK_DATA}
572
- value={bookmark}
573
- onChange={(async (item:any) => {
574
- setBookmark(item.value)
575
- })}
576
- />
577
- </View>
578
- <>{bookmark &&
579
- <TouchableRipple
580
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
581
- style={{
582
- padding:5,
583
- borderRadius:5,
584
- borderWidth:0,
585
- backgroundColor: "transparent",
586
- }}
587
- onPress={(async ()=>{
588
- const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
589
- if (stored_comic) setRemoveBookmark({...removeBookmark,state:true})
590
- else setBookmark("")
591
- })}
592
- >
593
- <Icon source={"tag-remove-outline"} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={"red"}/>
594
- </TouchableRipple>
595
- }</>
596
- </View>
597
- <View
598
- style={{
599
- display:"flex",
600
- flexDirection:"row",
601
- width:"100%",
602
- justifyContent:"space-around",
603
- alignItems:"center",
604
- }}
605
- >
606
- <Button mode='contained'
607
- labelStyle={{
608
- color:Theme[themeTypeContext].text_color,
609
- fontFamily:"roboto-medium",
610
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02
611
- }}
612
- style={{backgroundColor:"blue",borderRadius:5}}
613
- onPress={(()=>{
614
- setCreateBookmark({state:true,title:""})
615
- })}
616
- >+ Create Bookmark</Button>
617
- <Button mode='outlined'
618
- labelStyle={{
619
- color:Theme[themeTypeContext].text_color,
620
- fontFamily:"roboto-medium",
621
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
622
-
623
-
624
- }}
625
- style={{
626
-
627
- borderRadius:5,
628
- borderWidth:2,
629
- borderColor:Theme[themeTypeContext].border_color
630
- }}
631
- onPress={(async ()=>{
632
- if (defaultBookmark !== bookmark){
633
- const stored_comic = await ComicStorage.getByID(SOURCE,CONTENT.id)
634
- if (stored_comic) await ComicStorage.replaceTag(SOURCE, CONTENT.id, bookmark)
635
- else {
636
- const cover_result:any = await store_comic_cover(setShowCloudflareTurnstileContext,signal,SOURCE,ID,CONTENT)
637
-
638
- await ComicStorage.store(SOURCE,CONTENT.id, bookmark, {
639
- cover:cover_result,
640
- title:CONTENT.title,
641
- author:CONTENT.author,
642
- category:CONTENT.category,
643
- status:CONTENT.status,
644
- synopsis:CONTENT.synopsis,
645
- updated:CONTENT.updated,
646
- })
647
- }
648
- onRefresh()
649
- }
650
- setWidgetContext({state:false,component:<></>})
651
-
652
- })}
653
- >Done</Button>
654
- </View>
655
- </>
656
- }</>
657
-
658
- <>{createBookmark.state &&
659
- <>
660
- <View
661
- style={{
662
- height:"auto",
663
- display:"flex",
664
- flexDirection:"column",
665
- gap:12,
666
- }}
667
- >
668
- <TextInput mode="outlined" label="Create Bookmark" textColor={Theme[themeTypeContext].text_color} maxLength={72}
669
- placeholder="Bookmark Tag"
670
-
671
- right={<TextInput.Affix text={`| Max: 72`} />}
672
- style={{
673
-
674
- backgroundColor:Theme[themeTypeContext].background_color,
675
- borderColor:Theme[themeTypeContext].border_color,
676
-
677
- }}
678
- outlineColor={Theme[themeTypeContext].text_input_border_color}
679
- value={createBookmark.title}
680
- onChange={(event)=>{
681
- setCreateBookmark({...createBookmark,title:event.nativeEvent.text})
682
- }}
683
- />
684
- </View>
685
- <View
686
- style={{
687
- display:"flex",
688
- flexDirection:"row",
689
- width:"100%",
690
- justifyContent:"space-around",
691
- alignItems:"center",
692
- }}
693
- >
694
- <Button mode='outlined'
695
- labelStyle={{
696
- color:Theme[themeTypeContext].text_color,
697
- fontFamily:"roboto-medium",
698
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
699
-
700
-
701
- }}
702
- style={{
703
-
704
- borderRadius:5,
705
- borderWidth:2,
706
- borderColor:Theme[themeTypeContext].border_color
707
- }}
708
- onPress={(()=>{
709
-
710
- setCreateBookmark({...createBookmark,state:false})
711
-
712
- })}
713
- >Cancel</Button>
714
- <Button mode='contained'
715
- labelStyle={{
716
- color:Theme[themeTypeContext].text_color,
717
- fontFamily:"roboto-medium",
718
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02
719
- }}
720
- style={{backgroundColor:"green",borderRadius:5}}
721
- onPress={(async()=>{
722
-
723
- const title = createBookmark.title
724
- if (!title) return
725
-
726
- const stored_bookmark_data = await Storage.get("bookmark") || []
727
- if (stored_bookmark_data.includes(title)){
728
- Toast.show({
729
- type: 'error',
730
- text1: 'πŸ”– Duplicate Bookmark',
731
- text2: `Tag "${title}" already existed in your bookmark.`,
732
-
733
- position: "bottom",
734
- visibilityTime: 5000,
735
- text1Style:{
736
- fontFamily:"roboto-bold",
737
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
738
- },
739
- text2Style:{
740
- fontFamily:"roboto-medium",
741
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
742
-
743
- },
744
- });
745
- }else{
746
- await Storage.store("bookmark", [...stored_bookmark_data,title].sort())
747
- SET_BOOKMARK_DATA([...BOOKMARK_DATA,
748
- {label:title,value:title}
749
- ].sort())
750
- setCreateBookmark({state:false,title:""})
751
- Toast.show({
752
- type: 'info',
753
- text1: 'πŸ”– Create Bookmark',
754
- text2: `Tag "${title}" added to your bookmark.`,
755
-
756
- position: "bottom",
757
- visibilityTime: 3000,
758
- text1Style:{
759
- fontFamily:"roboto-bold",
760
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.025
761
- },
762
- text2Style:{
763
- fontFamily:"roboto-medium",
764
- fontSize:((Dimensions.width+Dimensions.height)/2)*0.0185,
765
-
766
- },
767
- });
768
- }
769
- })}
770
- >Add</Button>
771
- </View>
772
- </>
773
- }</>
774
- <>{removeBookmark.state &&
775
- <>
776
- <View
777
- style={{
778
- height:"auto",
779
- display:"flex",
780
- flexDirection:"column",
781
- gap:12,
782
- }}
783
- >
784
- <Text
785
- style={{
786
- color:Theme[themeTypeContext].text_color,
787
- fontFamily:"roboto-bold",
788
- fontSize:(Dimensions.width+Dimensions.height)/2*0.03,
789
- textAlign:"center",
790
- }}
791
- >Are you sure you want to remove this comic from bookmark?</Text>
792
-
793
- <Text
794
- style={{
795
- color:Theme[themeTypeContext].text_color,
796
- fontFamily:"roboto-medium",
797
- fontSize:(Dimensions.width+Dimensions.height)/2*0.025,
798
- textAlign:"center",
799
- }}
800
- >This will remove all local saved info and chapters.</Text>
801
- </View>
802
-
803
- <View
804
- style={{
805
- display:"flex",
806
- flexDirection:"row",
807
- width:"100%",
808
- justifyContent:"space-around",
809
- alignItems:"center",
810
- }}
811
- >
812
- <>{removeBookmark.removing
813
- ? <ActivityIndicator animating={true}/>
814
- :<>
815
-
816
- <Button mode='outlined' disabled={removeBookmark.removing}
817
- labelStyle={{
818
- color:Theme[themeTypeContext].text_color,
819
- fontFamily:"roboto-medium",
820
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02,
821
-
822
-
823
- }}
824
- style={{
825
-
826
- borderRadius:5,
827
- borderWidth:2,
828
- borderColor:Theme[themeTypeContext].border_color
829
- }}
830
- onPress={(()=>{
831
- setRemoveBookmark({...removeBookmark,state:false})
832
- })}
833
- >No</Button>
834
- <Button mode='contained' disabled={removeBookmark.removing}
835
- labelStyle={{
836
- color:Theme[themeTypeContext].text_color,
837
- fontFamily:"roboto-medium",
838
- fontSize:(Dimensions.width+Dimensions.height)/2*0.02
839
- }}
840
- style={{backgroundColor:"red",borderRadius:5}}
841
- onPress={(async ()=>{
842
- setRemoveBookmark({...removeBookmark,removing:false})
843
- if (Platform.OS !== "web"){
844
- const comic_dir = FileSystem.documentDirectory + "ComicMTL/" + `${SOURCE}/` + `${ID}/`
845
- await FileSystem.deleteAsync(comic_dir, { idempotent: true })
846
- }
847
-
848
- await ChapterStorage.drop(`${SOURCE}-${CONTENT.id}`)
849
- await ComicStorage.removeByID(SOURCE,CONTENT.id)
850
-
851
- onRefresh()
852
- setWidgetContext({state:false,component:<></>})
853
- })}
854
- >Yes</Button>
855
- </>
856
- }</>
857
-
858
- </View>
859
- </>
860
- }</>
861
-
862
- </View>
863
-
864
- }</>)
865
- }
 
 
 
1
 
2
+ import React, { useEffect, useState, useCallback, useContext, useRef, Fragment } from 'react';
3
+ import { Platform, useWindowDimensions, ScrollView } from 'react-native';
4
+
5
+ import { Icon, MD3Colors, Button, Text, TextInput, TouchableRipple, ActivityIndicator, Menu, Divider, PaperProvider, Portal } from 'react-native-paper';
6
  import { View, AnimatePresence } from 'moti';
7
  import Toast from 'react-native-toast-message';
8
  import * as FileSystem from 'expo-file-system';
 
12
  import Theme from '@/constants/theme';
13
  import Dropdown from '@/components/dropdown';
14
  import { CONTEXT } from '@/constants/module/context';
15
+ import { store_comic_cover } from '../../modules/content';
16
  import Storage from '@/constants/module/storages/storage';
17
  import ComicStorage from '@/constants/module/storages/comic_storage';
18
  import ImageCacheStorage from '@/constants/module/storages/image_cache_storage';
19
  import ChapterStorage from '@/constants/module/storages/chapter_storage';
20
 
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  interface RequestChapterWidgetProps {
23
  SOURCE: string | string[];
24
  ID: string | string[];
 
30
  get_requested_info: any;
31
  }
32
 
33
+ const RequestChapterWidget: React.FC<RequestChapterWidgetProps> = ({
34
  SOURCE,
35
  ID,
36
  CHAPTER,
 
318
  </View>)
319
  }
320
 
321
+ export default RequestChapterWidget;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/components/Image.tsx CHANGED
@@ -61,6 +61,7 @@ const Image = ({source, style, onError, contentFit, transition, onLoad, onLoadEn
61
 
62
  useFocusEffect(useCallback(() => {
63
  return () => {
 
64
  controller.abort();
65
  };
66
  },[]))
@@ -92,7 +93,10 @@ const Image = ({source, style, onError, contentFit, transition, onLoad, onLoadEn
92
  style={style}
93
  contentFit={contentFit}
94
  transition={transition}
95
- onLoad={onLoad}
 
 
 
96
  onLoadEnd={onLoadEnd}
97
  />
98
  : <View style={{...style,display:'flex',justifyContent:"center",alignItems:"center"}}>
 
61
 
62
  useFocusEffect(useCallback(() => {
63
  return () => {
64
+ imageData.current = null
65
  controller.abort();
66
  };
67
  },[]))
 
93
  style={style}
94
  contentFit={contentFit}
95
  transition={transition}
96
+ onLoad={()=>{
97
+ if (onLoad) onLoad()
98
+ imageData.current = null
99
+ }}
100
  onLoadEnd={onLoadEnd}
101
  />
102
  : <View style={{...style,display:'flex',justifyContent:"center",alignItems:"center"}}>
frontend/components/dropdown.tsx CHANGED
@@ -59,9 +59,11 @@ const __style = (Dimensions:any,theme_type:string) => StyleSheet.create({
59
  elevation: 5,
60
  },
61
  inputSearchStyle:{
 
62
  borderRadius:8,
63
  color: Theme[theme_type].text_color,
64
  fontSize: ((Dimensions.width+Dimensions.height)/2)*0.0225,
 
65
  },
66
  itemContainerStyle: {
67
  borderColor: Theme[theme_type].border_color,
 
59
  elevation: 5,
60
  },
61
  inputSearchStyle:{
62
+ borderWidth:0,
63
  borderRadius:8,
64
  color: Theme[theme_type].text_color,
65
  fontSize: ((Dimensions.width+Dimensions.height)/2)*0.0225,
66
+ padding:0,
67
  },
68
  itemContainerStyle: {
69
  borderColor: Theme[theme_type].border_color,
frontend/components/menu/components/menu_button.tsx CHANGED
@@ -23,28 +23,7 @@ const MenuButton = ({pathname, label, icon}:any) => {
23
 
24
 
25
  return (<>{style && <>
26
- <View style={style.menu_button_box} key={pathname}
27
- from={{
28
- opacity: 0,
29
- scale: 0.9,
30
- }}
31
- animate={{
32
- opacity: 1,
33
- scale: 1,
34
- }}
35
- exit={{
36
- opacity: 0,
37
- scale: 0.5,
38
- }}
39
- transition={{
40
- type: 'timing',
41
- duration: 500,
42
- }}
43
- exitTransition={{
44
- type: 'timing',
45
- duration: 250,
46
- }}
47
- >
48
  {current_pathname === pathname
49
  ? <TouchableRipple
50
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
@@ -56,22 +35,36 @@ const MenuButton = ({pathname, label, icon}:any) => {
56
  style={style.selected_menu_button}
57
  >
58
 
59
- <Icon source={icon} size={((Dimensions.width+Dimensions.height)/2)*0.045} color={Theme[themeTypeContext].icon_color}/>
60
 
61
  </TouchableRipple>
62
- : <><TouchableRipple
63
- rippleColor={Theme[themeTypeContext].ripple_color_outlined}
64
- onPress={() => {
 
 
 
 
65
 
66
- router.push(pathname)
67
- }}
68
-
69
- style={style.menu_button}
70
-
71
- >
72
- <Icon source={icon} size={((Dimensions.width+Dimensions.height)/2)*0.045} color={Theme[themeTypeContext].icon_color}/>
 
 
 
 
 
 
 
 
 
 
73
 
74
- </TouchableRipple><Text style={style.menu_button_text}>{label}</Text></>
75
  }
76
 
77
  </View>
 
23
 
24
 
25
  return (<>{style && <>
26
+ <View style={style.menu_button_box}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  {current_pathname === pathname
28
  ? <TouchableRipple
29
  rippleColor={Theme[themeTypeContext].ripple_color_outlined}
 
35
  style={style.selected_menu_button}
36
  >
37
 
38
+ <Icon source={icon} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={Theme[themeTypeContext].icon_color}/>
39
 
40
  </TouchableRipple>
41
+ : <>
42
+ <TouchableRipple
43
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
44
+ onPress={() => {
45
+
46
+ router.push(pathname)
47
+ }}
48
 
49
+ style={style.menu_button}
50
+
51
+ >
52
+ <View
53
+ style={{
54
+ display:"flex",
55
+ alignItems:"center",
56
+ justifyContent:"center",
57
+ gap:8,
58
+ width:"100%",
59
+ height:"100%",
60
+ }}
61
+ >
62
+ <Icon source={icon} size={((Dimensions.width+Dimensions.height)/2)*0.035} color={Theme[themeTypeContext].icon_color}/>
63
+ <Text selectable={false} style={style.menu_button_text}>{label}</Text>
64
+ </View>
65
+ </TouchableRipple>
66
 
67
+ </>
68
  }
69
 
70
  </View>
frontend/components/menu/menu.tsx CHANGED
@@ -1,40 +1,117 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { Link, usePathname } from 'expo-router';
3
  import { StyleSheet, View} from 'react-native';
4
  import { __styles } from './stylesheet/styles';
5
  import storage from '@/constants/module/storages/storage';
6
- import { Icon, MD3Colors, Button } from 'react-native-paper';
7
  import Theme from '@/constants/theme';
8
  import {useWindowDimensions} from 'react-native';
9
  import MenuButton from './components/menu_button';
10
 
 
 
 
11
  const Menu = () => {
12
  const [style, setStyle]:any = useState("")
13
- const [themeType, setThemeType]:any = useState("")
 
 
14
  const Dimensions = useWindowDimensions();
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
 
17
  useEffect(() => {
18
  (async ()=>{
19
- const theme_type = await storage.get("theme") || "DARK_GREEN"
20
- setThemeType(theme_type)
21
- setStyle(__styles(theme_type,Dimensions))
22
  })()
23
  },[])
24
 
25
 
26
- return (<>{style && themeType && <>
27
- {Dimensions.width <= 720
28
- ? <View
29
- style={style.menu_container}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  >
31
- <MenuButton pathname="/recent" label="Recent" icon="history"/>
32
- <MenuButton pathname="/bookmark" label="Bookmark" icon="bookmark"/>
33
- <MenuButton pathname="/explore" label="Explore" icon="compass"/>
34
- <MenuButton pathname="/setting" label="Setting" icon="cog"/>
35
- </View>
36
- : <></>
37
- }
38
 
39
  </>}</>);
40
  }
 
1
+ import React, { useEffect, useState, useContext, useCallback } from 'react';
2
+ import { Link, usePathname, useFocusEffect } from 'expo-router';
3
  import { StyleSheet, View} from 'react-native';
4
  import { __styles } from './stylesheet/styles';
5
  import storage from '@/constants/module/storages/storage';
6
+ import { Icon, MD3Colors, Button, Text, TouchableRipple } from 'react-native-paper';
7
  import Theme from '@/constants/theme';
8
  import {useWindowDimensions} from 'react-native';
9
  import MenuButton from './components/menu_button';
10
 
11
+ import { CONTEXT } from '@/constants/module/context';
12
+ import Storage from '@/constants/module/storages/storage';
13
+
14
  const Menu = () => {
15
  const [style, setStyle]:any = useState("")
16
+ const {themeTypeContext, setThemeTypeContext}:any = useContext(CONTEXT)
17
+ const [showMenuContext,setShowMenuContext]:any = useState(true)
18
+
19
  const Dimensions = useWindowDimensions();
20
+
21
+ useFocusEffect(useCallback(() => {
22
+ (async ()=>{
23
+ const MENU_STATE = await Storage.get("MENU_STATE")
24
+ if (MENU_STATE === null || MENU_STATE === undefined) setShowMenuContext(true)
25
+ else setShowMenuContext(MENU_STATE)
26
+
27
+ })()
28
+ return () => {
29
+
30
+ }
31
+ },[]))
32
 
33
 
34
  useEffect(() => {
35
  (async ()=>{
36
+ console.log(showMenuContext)
37
+ setStyle(__styles(themeTypeContext,Dimensions))
 
38
  })()
39
  },[])
40
 
41
 
42
+ return (<>{style && <>
43
+ <View
44
+ style={{
45
+ ...style.menu_container,
46
+ position: showMenuContext ? "relative" : "absolute",
47
+ height: showMenuContext ? "100%" : "auto",
48
+ backgroundColor: showMenuContext? Theme[themeTypeContext].background_color : "transparent",
49
+ marginBottom: showMenuContext ? 0 : Dimensions.height*0.015,
50
+ borderRightWidth: showMenuContext ? 0.5 : 0,
51
+
52
+ }}
53
+ >
54
+
55
+ <>{showMenuContext &&
56
+ <>
57
+ <View
58
+ style={{
59
+ paddingVertical: showMenuContext ? 12 : 18,
60
+ width:"100%",
61
+ height:"auto",
62
+ display:"flex",
63
+ justifyContent:"center",
64
+ alignItems:"center",
65
+ borderBottomWidth: 0.5,
66
+ borderColor: Theme[themeTypeContext].border_color,
67
+ }}
68
+ >
69
+
70
+ <Icon source={"menu-open"} size={((Dimensions.width+Dimensions.height)/2)*0.04} color={Theme[themeTypeContext].icon_color}/>
71
+ </View>
72
+ <View
73
+ style={style.menu_box}
74
+ >
75
+ <MenuButton pathname="/recent" label="Recent" icon="history"/>
76
+ <MenuButton pathname="/bookmark" label="Bookmark" icon="bookmark"/>
77
+ <MenuButton pathname="/explore" label="Explore" icon="compass"/>
78
+ <MenuButton pathname="/setting" label="Setting" icon="cog"/>
79
+ </View>
80
+ </>
81
+ }</>
82
+
83
+ <TouchableRipple
84
+ rippleColor={Theme[themeTypeContext].ripple_color_outlined}
85
+ onPress={async () => {
86
+ await Storage.store("MENU_STATE", !showMenuContext)
87
+ setShowMenuContext(!showMenuContext)
88
+ }}
89
+
90
+ style={{
91
+ backgroundColor: showMenuContext ? Theme[themeTypeContext].background_color : Theme[themeTypeContext].button_color,
92
+ borderTopRightRadius: showMenuContext ? 0 : ((Dimensions.height+Dimensions.width)/2)*0.015,
93
+ borderBottomRightRadius: showMenuContext ? 0 : ((Dimensions.height+Dimensions.width)/2)*0.015,
94
+ padding:8,
95
+ height:"auto",
96
+ width:"auto",
97
+ paddingVertical: showMenuContext ? 12 : 18,
98
+ justifyContent:"center",
99
+ alignItems:"center",
100
+
101
+ borderRightWidth: showMenuContext ? 0 : 0.5,
102
+ borderBottomWidth: showMenuContext ? 0 : 0.5,
103
+ borderTopWidth: showMenuContext ? 0.5 : 0.5,
104
+
105
+ borderColor: Theme[themeTypeContext].border_color,
106
+ }}
107
  >
108
+
109
+ <Icon source={showMenuContext ? "chevron-left" : "chevron-right"} size={((Dimensions.width+Dimensions.height)/2)*0.0375} color={Theme[themeTypeContext].icon_color}/>
110
+
111
+ </TouchableRipple>
112
+
113
+ </View>
114
+
115
 
116
  </>}</>);
117
  }
frontend/components/menu/stylesheet/styles.tsx CHANGED
@@ -4,18 +4,24 @@ import Theme from "@/constants/theme";
4
  export const __styles:any = (theme_type:string,Dimensions:any) => {
5
  return StyleSheet.create({
6
  menu_container: {
7
- position: "absolute",
8
  bottom: 0,
 
9
  display: "flex",
10
- flexDirection: "row",
11
- justifyContent: "space-around",
12
- alignItems: "center",
13
- width: "100%",
14
- backgroundColor: Theme[theme_type].background_color,
15
- padding: 8,
16
- borderTopWidth: 0.5,
17
  borderColor: Theme[theme_type].border_color,
18
  },
 
 
 
 
 
 
 
 
 
19
  menu_button_box:{
20
  display:"flex",
21
  alignItems:"center",
@@ -36,7 +42,8 @@ export const __styles:any = (theme_type:string,Dimensions:any) => {
36
 
37
  menu_button_text: {
38
  color: Theme[theme_type].text_color,
39
- fontSize: ((Dimensions.width+Dimensions.height)/2)*0.028,
 
40
  height:"auto",
41
  }
42
  })}
 
4
  export const __styles:any = (theme_type:string,Dimensions:any) => {
5
  return StyleSheet.create({
6
  menu_container: {
 
7
  bottom: 0,
8
+ left:0,
9
  display: "flex",
10
+ flexDirection: "column",
11
+ justifyContent: "space-between",
12
+ height:"auto",
13
+
 
 
 
14
  borderColor: Theme[theme_type].border_color,
15
  },
16
+ menu_box:{
17
+ flex:1,
18
+ display:"flex",
19
+ flexDirection:"column",
20
+ gap: 18,
21
+ alignItems:"center",
22
+ paddingHorizontal: Dimensions.width*0.005,
23
+ paddingVertical: 12,
24
+ },
25
  menu_button_box:{
26
  display:"flex",
27
  alignItems:"center",
 
42
 
43
  menu_button_text: {
44
  color: Theme[theme_type].text_color,
45
+ fontSize: ((Dimensions.width+Dimensions.height)/2)*0.02,
46
+ fontFamily: "roboto-light",
47
  height:"auto",
48
  }
49
  })}
frontend/components/navigation/TabBarIcon.tsx DELETED
@@ -1,9 +0,0 @@
1
- // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
2
-
3
- import Ionicons from '@expo/vector-icons/Ionicons';
4
- import { type IconProps } from '@expo/vector-icons/build/createIconSet';
5
- import { type ComponentProps } from 'react';
6
-
7
- export function TabBarIcon({ style, ...rest }: IconProps<ComponentProps<typeof Ionicons>['name']>) {
8
- return <Ionicons size={28} style={[{ marginBottom: -3 }, style]} {...rest} />;
9
- }
 
 
 
 
 
 
 
 
 
 
frontend/constants/module/storages/chapter_data_storage.tsx CHANGED
@@ -9,10 +9,13 @@ class Chapter_Data_Storage_Web {
9
  private static getDB(): Promise<IDBDatabase> {
10
  if (!this.dbPromise) {
11
  this.dbPromise = new Promise((resolve, reject) => {
12
- const request = indexedDB.open(DATABASE_NAME, 1);
13
 
14
  request.onupgradeneeded = (event) => {
15
  const db = (event.target as IDBOpenDBRequest).result;
 
 
 
16
  const store = db.createObjectStore('dataStore', { keyPath: 'id' });
17
  store.createIndex('comic_id', 'comic_id', { unique: false });
18
  store.createIndex('chapter_idx', 'chapter_idx', { unique: false });
@@ -87,24 +90,24 @@ class Chapter_Data_Storage_Web {
87
  });
88
  }
89
 
90
- static async removeByComicID(comic_id: string): Promise<void> {
91
  const db = await this.getDB();
92
  return new Promise((resolve, reject) => {
93
  const transaction = db.transaction('dataStore', 'readwrite');
94
  const store = transaction.objectStore('dataStore');
95
  const index = store.index('comic_id');
96
- const request = index.openCursor(comic_id);
97
-
98
  request.onsuccess = (event) => {
99
  const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
100
  if (cursor) {
101
- cursor.delete();
102
- cursor.continue();
103
  } else {
104
- resolve();
105
  }
106
  };
107
-
108
  request.onerror = () => {
109
  reject(request.error);
110
  };
 
9
  private static getDB(): Promise<IDBDatabase> {
10
  if (!this.dbPromise) {
11
  this.dbPromise = new Promise((resolve, reject) => {
12
+ const request = indexedDB.open(DATABASE_NAME, 3);
13
 
14
  request.onupgradeneeded = (event) => {
15
  const db = (event.target as IDBOpenDBRequest).result;
16
+
17
+ if (db.objectStoreNames.contains('dataStore')) db.deleteObjectStore('dataStore');
18
+
19
  const store = db.createObjectStore('dataStore', { keyPath: 'id' });
20
  store.createIndex('comic_id', 'comic_id', { unique: false });
21
  store.createIndex('chapter_idx', 'chapter_idx', { unique: false });
 
90
  });
91
  }
92
 
93
+ public static async removeByComicID(comic_id:string): Promise<void> {
94
  const db = await this.getDB();
95
  return new Promise((resolve, reject) => {
96
  const transaction = db.transaction('dataStore', 'readwrite');
97
  const store = transaction.objectStore('dataStore');
98
  const index = store.index('comic_id');
99
+ const request = index.openCursor(IDBKeyRange.only(comic_id));
100
+
101
  request.onsuccess = (event) => {
102
  const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
103
  if (cursor) {
104
+ store.delete(cursor.primaryKey);
105
+ cursor.continue();
106
  } else {
107
+ resolve();
108
  }
109
  };
110
+
111
  request.onerror = () => {
112
  reject(request.error);
113
  };
frontend/constants/module/storages/chapter_storage.tsx CHANGED
@@ -8,7 +8,7 @@ import { ensure_safe_table_name } from "../ensure_safe_table_name";
8
  const DATABASE_NAME = 'ChapterDB';
9
 
10
  class Chapter_Storage_Web {
11
- private static DATABASE_VERSION: number = 2;
12
 
13
  private static async openDB(): Promise<IDBDatabase> {
14
  return new Promise((resolve, reject) => {
@@ -16,11 +16,13 @@ class Chapter_Storage_Web {
16
 
17
  request.onupgradeneeded = (event) => {
18
  const db = (event.target as IDBOpenDBRequest).result;
19
- if (!db.objectStoreNames.contains('dataStore')) {
20
- const store = db.createObjectStore('dataStore', { keyPath: 'id' });
21
- store.createIndex('item', 'item', { unique: false });
22
- store.createIndex('idx', 'idx', { unique: false });
23
- }
 
 
24
  };
25
 
26
  request.onsuccess = () => {
 
8
  const DATABASE_NAME = 'ChapterDB';
9
 
10
  class Chapter_Storage_Web {
11
+ private static DATABASE_VERSION: number = 3;
12
 
13
  private static async openDB(): Promise<IDBDatabase> {
14
  return new Promise((resolve, reject) => {
 
16
 
17
  request.onupgradeneeded = (event) => {
18
  const db = (event.target as IDBOpenDBRequest).result;
19
+
20
+ if (db.objectStoreNames.contains('dataStore')) db.deleteObjectStore('dataStore');
21
+
22
+ const store = db.createObjectStore('dataStore', { keyPath: 'id' });
23
+ store.createIndex('item', 'item', { unique: false });
24
+ store.createIndex('idx', 'idx', { unique: false });
25
+
26
  };
27
 
28
  request.onsuccess = () => {
frontend/constants/module/storages/comic_storage.tsx CHANGED
@@ -1,5 +1,7 @@
1
  import { Platform } from "react-native";
2
  import * as SQLite from 'expo-sqlite';
 
 
3
 
4
  const DATABASE_NAME = 'ComicStorageDB'
5
 
@@ -9,10 +11,11 @@ class Comic_Storage_Web {
9
  private static getDB(): Promise<IDBDatabase> {
10
  if (!this.dbPromise) {
11
  this.dbPromise = new Promise((resolve, reject) => {
12
- const request = indexedDB.open(DATABASE_NAME, 2);
13
 
14
  request.onupgradeneeded = (event) => {
15
  const db = (event.target as IDBOpenDBRequest).result;
 
16
  const store = db.createObjectStore('dataStore', { keyPath: 'id' });
17
  store.createIndex('tag', 'tag', { unique: false });
18
  store.createIndex('source', 'source', { unique: false });
@@ -256,8 +259,17 @@ class Comic_Storage_Web {
256
  request.onsuccess = (event) => {
257
  const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
258
  if (cursor) {
 
 
 
 
 
 
 
 
259
  cursor.delete();
260
  cursor.continue();
 
261
  } else {
262
  resolve();
263
  }
 
1
  import { Platform } from "react-native";
2
  import * as SQLite from 'expo-sqlite';
3
+ import ChapterStorage from "./chapter_storage";
4
+ import ChapterDataStorage from "./chapter_data_storage";
5
 
6
  const DATABASE_NAME = 'ComicStorageDB'
7
 
 
11
  private static getDB(): Promise<IDBDatabase> {
12
  if (!this.dbPromise) {
13
  this.dbPromise = new Promise((resolve, reject) => {
14
+ const request = indexedDB.open(DATABASE_NAME, 3);
15
 
16
  request.onupgradeneeded = (event) => {
17
  const db = (event.target as IDBOpenDBRequest).result;
18
+ if (db.objectStoreNames.contains('dataStore')) db.deleteObjectStore('dataStore');
19
  const store = db.createObjectStore('dataStore', { keyPath: 'id' });
20
  store.createIndex('tag', 'tag', { unique: false });
21
  store.createIndex('source', 'source', { unique: false });
 
259
  request.onsuccess = (event) => {
260
  const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
261
  if (cursor) {
262
+
263
+ const data = cursor.value;
264
+ const source = data.source
265
+ const comic_id = data.id;
266
+
267
+ ChapterStorage.drop(`${source}-${comic_id}`),
268
+ ChapterDataStorage.removeByComicID(comic_id)
269
+
270
  cursor.delete();
271
  cursor.continue();
272
+
273
  } else {
274
  resolve();
275
  }
frontend/constants/module/storages/image_cache_storage.tsx CHANGED
@@ -25,13 +25,13 @@ class ImageStorage_Web {
25
  // Initialize the database
26
  private static async initDB(): Promise<IDBDatabase> {
27
  return new Promise((resolve, reject) => {
28
- const request = indexedDB.open(DATABASE_NAME, 1);
29
 
30
  request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
31
  const db = (event.target as IDBOpenDBRequest).result;
32
- if (!db.objectStoreNames.contains('images')) {
33
- db.createObjectStore('images', { keyPath: 'link' });
34
- }
35
  };
36
 
37
  request.onsuccess = () => {
 
25
  // Initialize the database
26
  private static async initDB(): Promise<IDBDatabase> {
27
  return new Promise((resolve, reject) => {
28
+ const request = indexedDB.open(DATABASE_NAME, 3);
29
 
30
  request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
31
  const db = (event.target as IDBOpenDBRequest).result;
32
+ if (db.objectStoreNames.contains('images')) db.deleteObjectStore('images');
33
+ db.createObjectStore('images', { keyPath: 'link' });
34
+
35
  };
36
 
37
  request.onsuccess = () => {
frontend/constants/module/storages/storage.tsx CHANGED
@@ -9,10 +9,11 @@ class Storage_Web {
9
  private static getDB(): Promise<IDBDatabase> {
10
  if (!this.dbPromise) {
11
  this.dbPromise = new Promise((resolve, reject) => {
12
- const request = indexedDB.open(DATABASE_NAME, 1);
13
 
14
  request.onupgradeneeded = (event) => {
15
  const db = (event.target as IDBOpenDBRequest).result;
 
16
  db.createObjectStore('dataStore');
17
  };
18
 
 
9
  private static getDB(): Promise<IDBDatabase> {
10
  if (!this.dbPromise) {
11
  this.dbPromise = new Promise((resolve, reject) => {
12
+ const request = indexedDB.open(DATABASE_NAME, 2);
13
 
14
  request.onupgradeneeded = (event) => {
15
  const db = (event.target as IDBOpenDBRequest).result;
16
+ if (db.objectStoreNames.contains('dataStore')) db.deleteObjectStore('dataStore');
17
  db.createObjectStore('dataStore');
18
  };
19