tomoya 1 سال پیش
والد
کامیت
f903b72198
59فایلهای تغییر یافته به همراه1402 افزوده شده و 229 حذف شده
  1. 1 0
      backend/app/.gitignore
  2. 2 1
      backend/app/app/api/api_v1/api.py
  3. 27 31
      backend/app/app/api/api_v1/endpoints/images.py
  4. 36 8
      backend/app/app/api/api_v1/endpoints/login.py
  5. 2 2
      backend/app/app/api/api_v1/endpoints/reputations.py
  6. 41 0
      backend/app/app/api/api_v1/endpoints/ser_no.py
  7. 105 12
      backend/app/app/api/api_v1/endpoints/videos.py
  8. 12 0
      backend/app/app/api/deps.py
  9. 1 1
      backend/app/app/core/celery_app.py
  10. 1 0
      backend/app/app/core/celeryconf.py
  11. 2 1
      backend/app/app/crud/__init__.py
  12. 1 1
      backend/app/app/crud/crud_article.py
  13. 25 0
      backend/app/app/crud/crud_ser_no.py
  14. 3 3
      backend/app/app/crud/crud_video.py
  15. 2 0
      backend/app/app/db/base.py
  16. 3 1
      backend/app/app/models/__init__.py
  17. 14 0
      backend/app/app/models/serial_number.py
  18. 1 1
      backend/app/app/models/user.py
  19. 1 1
      backend/app/app/models/video.py
  20. 10 0
      backend/app/app/models/voice.py
  21. 1 0
      backend/app/app/schemas/__init__.py
  22. 21 0
      backend/app/app/schemas/serial_number.py
  23. 1 0
      backend/app/app/schemas/user.py
  24. 2 2
      backend/app/app/schemas/video.py
  25. 46 34
      backend/app/app/worker.py
  26. 12 9
      frontend/src/App.vue
  27. 46 18
      frontend/src/api.ts
  28. BIN
      frontend/src/assets/img/qrcode/Angela.png
  29. BIN
      frontend/src/assets/img/qrcode/aianchor3-title.png
  30. BIN
      frontend/src/assets/img/qrcode/aianchor3bg.png
  31. BIN
      frontend/src/assets/img/qrcode/angela.webp
  32. BIN
      frontend/src/assets/img/qrcode/button1.png
  33. BIN
      frontend/src/assets/img/qrcode/button2.png
  34. BIN
      frontend/src/assets/img/qrcode/button3.png
  35. BIN
      frontend/src/assets/img/qrcode/choozmologo.png
  36. BIN
      frontend/src/assets/img/qrcode/icon-19.png
  37. BIN
      frontend/src/assets/img/qrcode/icon-20.png
  38. BIN
      frontend/src/assets/img/qrcode/line1.png
  39. BIN
      frontend/src/assets/img/qrcode/line2.png
  40. BIN
      frontend/src/assets/img/qrcode/moichiu.png
  41. BIN
      frontend/src/assets/img/qrcode/moichiu.webp
  42. BIN
      frontend/src/assets/img/qrcode/startline.png
  43. BIN
      frontend/src/assets/img/qrcode/startline1.png
  44. BIN
      frontend/src/assets/img/qrcode/儲值卷-06.png
  45. BIN
      frontend/src/assets/img/qrcode/儲值卷-07.png
  46. BIN
      frontend/src/assets/img/qrcode/儲值卷-13.png
  47. 14 4
      frontend/src/components/Dialog.vue
  48. 1 0
      frontend/src/components/Navbar.vue
  49. 1 1
      frontend/src/env.ts
  50. 3 1
      frontend/src/interfaces/index.ts
  51. 2 0
      frontend/src/language/en.json
  52. 2 0
      frontend/src/language/zh.json
  53. 12 7
      frontend/src/router/index.ts
  54. 99 43
      frontend/src/stores/main.ts
  55. 1 0
      frontend/src/views/Login.vue
  56. 766 0
      frontend/src/views/Qrcode.vue
  57. 9 3
      frontend/src/views/main/Dashboard.vue
  58. 32 7
      frontend/src/views/main/Image.vue
  59. 41 37
      frontend/src/views/main/Start.vue

+ 1 - 0
backend/app/.gitignore

@@ -1,3 +1,4 @@
 .mypy_cache
 .mypy_cache
 .coverage
 .coverage
 htmlcov
 htmlcov
+app/worker.py

+ 2 - 1
backend/app/app/api/api_v1/api.py

@@ -1,6 +1,6 @@
 from fastapi import APIRouter
 from fastapi import APIRouter
 
 
-from app.api.api_v1.endpoints import  login, users, utils, videos, images, reputations
+from app.api.api_v1.endpoints import  login, users, utils, videos, images, reputations, ser_no
 
 
 api_router = APIRouter()
 api_router = APIRouter()
 api_router.include_router(login.router, tags=["login"])
 api_router.include_router(login.router, tags=["login"])
@@ -9,3 +9,4 @@ api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
 api_router.include_router(videos.router, prefix="/videos", tags=["videos"])
 api_router.include_router(videos.router, prefix="/videos", tags=["videos"])
 api_router.include_router(images.router, prefix="/images", tags=["iamges"])
 api_router.include_router(images.router, prefix="/images", tags=["iamges"])
 api_router.include_router(reputations.router, prefix="/reputations", tags=["reputations"])
 api_router.include_router(reputations.router, prefix="/reputations", tags=["reputations"])
+api_router.include_router(ser_no.router, prefix="/ser_nos", tags=["serial numbers"])

+ 27 - 31
backend/app/app/api/api_v1/endpoints/images.py

@@ -26,7 +26,7 @@ LOCAL_ZIP_STORAGE = Path("/").joinpath(settings.LOCAL_ZIP_STORAGE)
 
 
 
 
 router = APIRouter()
 router = APIRouter()
-
+sr_clients = {}
 
 
 @router.post("/sr")
 @router.post("/sr")
 def supser_resolution(
 def supser_resolution(
@@ -55,42 +55,35 @@ def supser_resolution(
           return {"error": str(e)}
           return {"error": str(e)}
       finally:
       finally:
           upload_files[i].file.close()
           upload_files[i].file.close()
-    
+          
     background_tasks.add_task(wait_finish, new_dir, filenames)
     background_tasks.add_task(wait_finish, new_dir, filenames)
 
 
     print(filenames)
     print(filenames)
-    return JSONResponse({"filenames": stemnames}, background=background_tasks)
+    return JSONResponse({"filenames": filenames}, background=background_tasks)
 
 
 async def wait_finish(dirname, filenames):
 async def wait_finish(dirname, filenames):
-    # print("start: "+str(datetime.now()))
-    new_dir_path = Path(BACKEND_ZIP_STORAGE).joinpath(dirname)
     process = await asyncio.create_subprocess_exec("sshpass", "-p", "choozmo9", 
     process = await asyncio.create_subprocess_exec("sshpass", "-p", "choozmo9", 
-                    "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", "-r", f"{str(new_dir_path)}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE)}")
+                     "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", "-r", f"{str(BACKEND_ZIP_STORAGE/dirname)}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE)}")
     await process.wait()
     await process.wait()
     # r = subprocess.run(["sshpass", "-p", "choozmo9", 
     # r = subprocess.run(["sshpass", "-p", "choozmo9", 
     #                 "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", "-r", f"{str(new_dir_path)}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE)}"])
     #                 "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", "-r", f"{str(new_dir_path)}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE)}"])
-    
-    restored_imgs = "restored_imgs" 
-    res = celery_app.send_task("app.worker.super_resolution", args=[dirname])
-    # print(res.state)
+    res = celery_app.send_task("app.worker.super_resolution", args=[dirname, filenames])
+
     while True:
     while True:
-        await asyncio.sleep(0.5)
-        print(res.state)
-        if res.status == 'SUCCESS':
-            print("recieve finished")
-            break
+       await asyncio.sleep(0.5)
+       if res.state == "SUCCESS":
+           break
+    restored_imgs = "restored_imgs"
+    process = await asyncio.create_subprocess_exec("sshpass", "-p", "choozmo9", 
+                     "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", "-r", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE/dirname/restored_imgs)}/*", f"{str(BACKEND_ZIP_STORAGE)}")
+    await process.wait()
     for filename in filenames:
     for filename in filenames:
-        process = await asyncio.create_subprocess_exec("sshpass", "-p", "choozmo9", 
-                      "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", "-r", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE/dirname/restored_imgs/filename)}", f"{str(BACKEND_ZIP_STORAGE)}")
-        await process.wait()
-        # re = subprocess.run(["sshpass", "-p", "choozmo9", 
-        #             "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", "-r", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE/dirname/restored_imgs/filename)}", f"{str(BACKEND_ZIP_STORAGE)}"])
-        
-        for sr_client in sr_clients.values():
-            await sr_client.send_text(f"{filename}")
-    # print("end: "+str(datetime.now()))
+        await publish(filename)
 
 
-    
+
+async def publish(data):
+    for sr_client in sr_clients.values():
+        await sr_client.send_text(f"{data}")
 
 
 @router.get("/sr")
 @router.get("/sr")
 def get_image(
 def get_image(
@@ -103,12 +96,17 @@ def get_image(
     """
     """
     Download image
     Download image
     """
     """
+    return_files = list(BACKEND_ZIP_STORAGE.glob(stored_file_name))
+    if return_files:
+        return_file = return_files[0]
+        return FileResponse(path=str(return_file), filename=file_name+"_hr"+return_file.suffix)
+    else:
+        print("non")
     
     
-    return_file_path = list(BACKEND_ZIP_STORAGE.glob(stored_file_name+"*"))[0]
-    return FileResponse(path=str(return_file_path), media_type='image/png', filename=file_name+return_file_path.suffix)
 
 
-ws_clients = {}
-sr_clients = {}
+@router.delete("/sr")
+def del_image():
+    pass
 
 
 @router.websocket("/sr")
 @router.websocket("/sr")
 async def websocket_endpoint(websocket: WebSocket):
 async def websocket_endpoint(websocket: WebSocket):
@@ -122,11 +120,9 @@ async def websocket_endpoint(websocket: WebSocket):
             print(f"{key}:{data}")
             print(f"{key}:{data}")
             if data.startswith("unsubscribe"):
             if data.startswith("unsubscribe"):
               del sr_clients[key]
               del sr_clients[key]
-              print(f"beybey: {key}")
               #for client in sr_clients.values():
               #for client in sr_clients.values():
               #      await client.send_text(f"ID: {key} | Message: {data}")
               #      await client.send_text(f"ID: {key} | Message: {data}")
 
 
     except:
     except:
-        #await websocket.close()
         # 接続が切れた場合、当該クライアントを削除する
         # 接続が切れた場合、当該クライアントを削除する
         del sr_clients[key]
         del sr_clients[key]

+ 36 - 8
backend/app/app/api/api_v1/endpoints/login.py

@@ -1,10 +1,10 @@
 from datetime import timedelta
 from datetime import timedelta
-from typing import Any
+from typing import Any, Optional
 
 
 from fastapi import APIRouter, Body, Depends, HTTPException
 from fastapi import APIRouter, Body, Depends, HTTPException
 from fastapi.security import OAuth2PasswordRequestForm
 from fastapi.security import OAuth2PasswordRequestForm
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
-
+from datetime import datetime
 from app import crud, models, schemas
 from app import crud, models, schemas
 from app.api import deps
 from app.api import deps
 from app.core import security
 from app.core import security
@@ -22,9 +22,9 @@ from google.auth.transport import requests
 router = APIRouter()
 router = APIRouter()
 
 
 
 
-@router.post("/login/access-token", response_model=schemas.Token)
+@router.post("/login/access-token")#, response_model=schemas.Token)
 def login_access_token(
 def login_access_token(
-    db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends()
+    db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends(), add_time_code: Optional[str] = None
 ) -> Any:
 ) -> Any:
     """
     """
     OAuth2 compatible token login, get an access token for future requests
     OAuth2 compatible token login, get an access token for future requests
@@ -37,16 +37,30 @@ def login_access_token(
     elif not crud.user.is_active(user):
     elif not crud.user.is_active(user):
         raise HTTPException(status_code=400, detail="Inactive user")
         raise HTTPException(status_code=400, detail="Inactive user")
     access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
     access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
-    return {
+    return_msg = {
         "access_token": security.create_access_token(
         "access_token": security.create_access_token(
             user.id, expires_delta=access_token_expires
             user.id, expires_delta=access_token_expires
         ),
         ),
         "token_type": "bearer",
         "token_type": "bearer",
     }
     }
+    if add_time_code:
+        available_ser_no = crud.serial_number.available(db, ser_no=add_time_code)
+        print(available_ser_no)
+        if available_ser_no:
+            user_in = schemas.UserUpdate(available_time=user.available_time+available_ser_no.time)
+            crud.user.update(db, db_obj=user, obj_in=user_in)
+        
+            ser_no_in = schemas.SerialNumberUpdate(code=available_ser_no.code, is_used=True, used_datetime=str(datetime.now()), owner_id=user.id)
+            crud.serial_number.update(db, db_obj=available_ser_no, obj_in=ser_no_in)
+            print(available_ser_no.time, type(available_ser_no.time))
+            return_msg['time_added'] = available_ser_no.time
+        else:
+            return_msg['time_added'] = -1
+    return return_msg 
 
 
-@router.post("/login/google/access-token", response_model=schemas.Token)
+@router.post("/login/google/access-token")#, response_model=schemas.Token)
 def login_access_token(
 def login_access_token(
-    db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends()
+    db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends(), add_time_code: Optional[str] = None
 ) -> Any:
 ) -> Any:
     """
     """
     OAuth2 compatible token login, get an access token for future requests
     OAuth2 compatible token login, get an access token for future requests
@@ -60,12 +74,26 @@ def login_access_token(
     elif not crud.user.is_active(user):
     elif not crud.user.is_active(user):
         raise HTTPException(status_code=400, detail="Inactive user")
         raise HTTPException(status_code=400, detail="Inactive user")
     access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
     access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
-    return {
+    return_msg = {
         "access_token": security.create_access_token(
         "access_token": security.create_access_token(
             user.id, expires_delta=access_token_expires
             user.id, expires_delta=access_token_expires
         ),
         ),
         "token_type": "bearer",
         "token_type": "bearer",
     }
     }
+    if add_time_code:
+        available_ser_no = crud.serial_number.available(db, ser_no=add_time_code)
+        print(available_ser_no)
+        if available_ser_no:
+            user_in = schemas.UserUpdate(available_time=user.available_time+available_ser_no.time)
+            crud.user.update(db, db_obj=user, obj_in=user_in)
+        
+            ser_no_in = schemas.SerialNumberUpdate(code=available_ser_no.code, is_used=True, used_datetime=str(datetime.now()), owner_id=user.id)
+            crud.serial_number.update(db, db_obj=available_ser_no, obj_in=ser_no_in)
+            print(available_ser_no.time, type(available_ser_no.time))
+            return_msg['time_added'] = available_ser_no.time
+        else:
+            return_msg['time_added'] = -1
+    return return_msg 
 
 
 
 
 
 

+ 2 - 2
backend/app/app/api/api_v1/endpoints/reputations.py

@@ -36,6 +36,6 @@ async def post_reputation(
     """
     """
     
     
     #print(posted_article)
     #print(posted_article)
-    article = crud.artivle.create_with_owner(db=db, obj_in=posted_article, owner_id=current_user.id, posted_datetime=str(datetime.now()))
+    article = crud.article.create_with_owner(db=db, obj_in=posted_article, owner_id=current_user.id, posted_datetime=str(datetime.now()))
     if article:
     if article:
-      return {"id":article.id}
+        return {"id":article.id}

+ 41 - 0
backend/app/app/api/api_v1/endpoints/ser_no.py

@@ -0,0 +1,41 @@
+from typing import Any, List
+from fastapi.responses import RedirectResponse
+from fastapi import APIRouter, Body, Depends, HTTPException, Request
+from fastapi.encoders import jsonable_encoder
+from pydantic.networks import EmailStr
+from sqlalchemy.orm import Session
+
+from app import crud, models, schemas
+from app.api import deps
+from app.core.config import settings
+
+from datetime import datetime
+
+router = APIRouter()
+
+@router.get("/add-time")
+def add_time(
+    *,
+    db: Session = Depends(deps.get_db),
+    current_user: models.User = Depends(deps.get_current_active_user),
+    ser_no: models.SerialNumber = Depends(deps.get_avairable_serial_number)
+)-> Any:
+
+  
+    user_in = schemas.UserUpdate(available_time=current_user.available_time+ser_no.time)
+    crud.user.update(db, db_obj=current_user, obj_in=user_in)
+    
+    ser_no_in = schemas.SerialNumberUpdate(code=ser_no.code, is_used=True, used_datetime=str(datetime.now()), owner_id=current_user.id)
+    crud.serial_number.update(db, db_obj=ser_no, obj_in=ser_no_in)
+
+    return {"time_added": ser_no.time}
+
+@router.get("/add-time-no-token")
+def add_time_no_token(
+    request: Request,
+    ser_no: models.SerialNumber = Depends(deps.get_avairable_serial_number)
+)-> Any:
+    print(f"{request.base_url}login?ser_no={ser_no.code}")
+    #return RedirectResponse(f"{request.base_url}login?ser_no={ser_no.code}")
+    return RedirectResponse(f"http://localhost:5173/login?add-time-code={ser_no.code}")
+

+ 105 - 12
backend/app/app/api/api_v1/endpoints/videos.py

@@ -1,10 +1,10 @@
 from typing import Any, List, Optional
 from typing import Any, List, Optional
 import subprocess
 import subprocess
 from fastapi import UploadFile, File, Form
 from fastapi import UploadFile, File, Form
-from fastapi.responses import FileResponse
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi.responses import FileResponse, JSONResponse
+from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, WebSocket
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
-
+from fastapi.encoders import jsonable_encoder
 import app.crud as crud
 import app.crud as crud
 import app.models as models
 import app.models as models
 import app.schemas as schemas 
 import app.schemas as schemas 
@@ -13,14 +13,16 @@ from app.api import deps
 from app.core.celery_app import celery_app
 from app.core.celery_app import celery_app
 from app.core.config import settings
 from app.core.config import settings
 from pathlib import Path
 from pathlib import Path
+from app.db.session import SessionLocal
 
 
-from app.core.celery_app import celery_app
+import asyncio
 
 
 BACKEND_ZIP_STORAGE = Path("/app").joinpath(settings.BACKEND_ZIP_STORAGE)
 BACKEND_ZIP_STORAGE = Path("/app").joinpath(settings.BACKEND_ZIP_STORAGE)
 LOCAL_ZIP_STORAGE = Path("/").joinpath(settings.LOCAL_ZIP_STORAGE)
 LOCAL_ZIP_STORAGE = Path("/").joinpath(settings.LOCAL_ZIP_STORAGE)
 
 
 
 
 router = APIRouter()
 router = APIRouter()
+video_clients = {}
 
 
 @router.get("/", response_model=List[schemas.Video])
 @router.get("/", response_model=List[schemas.Video])
 def get_video_list(
 def get_video_list(
@@ -40,6 +42,20 @@ def get_video_list(
         )
         )
     return videos
     return videos
 
 
+@router.post("/test")
+def test(
+    *,
+    title: str,
+    anchor_id: int,
+    lang_id: int,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    video_data = {"title":title, "anchor_id":anchor_id, "lang_id":lang_id}
+    print(video_data)
+    task = celery_app.send_task("app.worker.make_video_test",  kwargs=video_data, )
+    print(task)
+    return "ok"
+
 @router.post("/", response_model=schemas.Video)
 @router.post("/", response_model=schemas.Video)
 def upload_plot(
 def upload_plot(
     *,
     *,
@@ -49,16 +65,17 @@ def upload_plot(
     lang_id: int=Form(...),
     lang_id: int=Form(...),
     upload_file: UploadFile=File(),
     upload_file: UploadFile=File(),
     current_user: models.User = Depends(deps.get_current_active_user),
     current_user: models.User = Depends(deps.get_current_active_user),
+    background_tasks: BackgroundTasks,
 ) -> Any:
 ) -> Any:
     """
     """
     Create new video.
     Create new video.
     """
     """
     print(title)
     print(title)
     print(upload_file.filename)
     print(upload_file.filename)
-    file_name = crud.video.generate_file_name(db=db, n=20)
+    filename = crud.video.generate_file_name(db=db, n=20)
     
     
     try:
     try:
-        with open(str(Path(BACKEND_ZIP_STORAGE).joinpath(video.stored_file_name+".zip")), 'wb') as f:
+        with open(str(Path(BACKEND_ZIP_STORAGE).joinpath(filename+".zip")), 'wb') as f:
             while contents := upload_file.file.read(1024 * 1024):
             while contents := upload_file.file.read(1024 * 1024):
                 f.write(contents)
                 f.write(contents)
     except Exception as e:
     except Exception as e:
@@ -66,18 +83,76 @@ def upload_plot(
         return {"error": str(e)}
         return {"error": str(e)}
     finally:
     finally:
         upload_file.file.close()
         upload_file.file.close()
+    
 
 
-    # check valid file
-    video_create = schemas.VideoCreate(title=title, progress_state="waiting", stored_file_name=file_name)
-    video = crud.video.create_with_owner(db=db, obj_in=video_create, owner_id=current_user.id)
-
+    '''
     zip_filename = video.stored_file_name+".zip"
     zip_filename = video.stored_file_name+".zip"
     print(str(BACKEND_ZIP_STORAGE/zip_filename))
     print(str(BACKEND_ZIP_STORAGE/zip_filename))
     r = subprocess.run(["sshpass", "-p", "choozmo9", 
     r = subprocess.run(["sshpass", "-p", "choozmo9", 
                     "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", f"{str(BACKEND_ZIP_STORAGE/zip_filename)}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE/zip_filename)}"])
                     "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", f"{str(BACKEND_ZIP_STORAGE/zip_filename)}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE/zip_filename)}"])
     print(r.returncode)
     print(r.returncode)
-    celery_app.send_task("app.worker.make_video", args=[video.id, video.stored_file_name, current_user.id])
-    return video
+    celery_app.send_task("app.worker.make_video", args=[video.id, video.stored_file_name, current_user.id, anchor_id, current_user.membership_status, current_user.available_time])
+    '''
+    video_create = schemas.VideoCreate(title=title, progress_state="PENDING", stored_filename=filename)
+    video = crud.video.create_with_owner(db=db, obj_in=video_create, owner_id=current_user.id)
+    return_msg = {"video_message":"accepted"}
+    video_data = jsonable_encoder(video)
+    video_data['membership_status'] = current_user.membership_status
+    video_data['available_time'] = current_user.available_time
+    video_data['video_id'] = video_data['id']
+    background_tasks.add_task(wait_finish, video_data)
+    return  JSONResponse(return_msg, background=background_tasks)
+
+async def wait_finish(video_data:dict): 
+    zip_filename = video_data['stored_filename']+".zip"
+    process = await asyncio.create_subprocess_exec("sshpass", "-p", "choozmo9", 
+                    "scp", "-P", "5722", "-o", "StrictHostKeyChecking=no", f"{str(BACKEND_ZIP_STORAGE/zip_filename)}", f"root@172.104.93.163:{str(LOCAL_ZIP_STORAGE)}")
+    await process.wait()
+
+    task = celery_app.send_task("app.worker.make_video", kwargs=video_data)
+    while True:
+       await asyncio.sleep(1)
+       if task.state != "PENDING":
+           break
+       
+    db = SessionLocal()
+    video = db.query(models.Video).get(video_data['id'])
+    video.progress_state = "STARTED" 
+    db.commit()
+    db.close()
+    msg_data = f"{video_data['stored_filename']}:STARTED"
+    await publish(msg_data)
+
+    while True:
+        await asyncio.sleep(1)
+        if task.state != "STARTED":
+            break
+
+    if task.state == "SUCCESS":
+        db = SessionLocal()
+        video = db.query(models.Video).get(video_data['id'])
+        user = db.query(models.User).get(video_data['owner_id'])
+        video.progress_state = "SUCCESS" 
+        if time := task.result:
+            user.available_time -= int(time)
+            video.length = int(time)
+        db.commit()
+        db.close()
+        msg_data = f"{video_data['stored_filename']}:SUCCESS:{int(time)}"
+
+    elif task.state == "FAILURE":
+        db = SessionLocal()
+        video = db.query(models.Video).get(video_data['id'])
+        video.progress_state = "FAILURE" 
+        db.commit()
+        db.close()
+        msg_data = f"{video_data['stored_filename']}:FAILURE"
+
+    await publish(msg_data)
+
+async def publish(data):
+    for video_client in video_clients.values():
+        await video_client.send_text(f"{data}")
 
 
 @router.get("/{id}")
 @router.get("/{id}")
 def download_video(
 def download_video(
@@ -89,3 +164,21 @@ def download_video(
     
     
     return {"message":"address"}
     return {"message":"address"}
 
 
+@router.websocket("")
+async def websocket_endpoint(websocket: WebSocket):
+    await websocket.accept()
+    key = websocket.headers.get('sec-websocket-key')
+    video_clients[key] = websocket
+    try:
+        while True:
+            data = await websocket.receive_text()
+            if not data.startswith("subscribe"):
+              del video_clients[key]
+              #for client in sr_clients.values():
+              #      await client.send_text(f"ID: {key} | Message: {data}")
+
+    except:
+        # 接続が切れた場合、当該クライアントを削除する
+        del video_clients[key]
+
+

+ 12 - 0
backend/app/app/api/deps.py

@@ -59,3 +59,15 @@ def get_current_active_superuser(
             status_code=400, detail="The user doesn't have enough privileges"
             status_code=400, detail="The user doesn't have enough privileges"
         )
         )
     return current_user
     return current_user
+
+def get_valid_serial_number(code: str, db: Session = Depends(get_db)):
+    ser_no = db.query(models.SerialNumber).filter(models.SerialNumber.code==code).first()
+    if not ser_no:
+        raise HTTPException(status_code=400, detail="This serial number is invalid")
+    
+    return ser_no
+
+def get_avairable_serial_number(ser_no:models.SerialNumber = Depends(get_valid_serial_number)):
+    if ser_no.is_used:
+        raise HTTPException(status_code=400, detail="This serial number is already used")
+    return ser_no

+ 1 - 1
backend/app/app/core/celery_app.py

@@ -4,4 +4,4 @@ celery_app = Celery("worker", broker="redis://172.104.93.163:16379/0", backend="
 
 
 
 
 
 
-celery_app.conf.task_routes = {"app.worker.make_video": "main-queue", "app.worker.super_resolution": "main-queue"}
+celery_app.conf.task_routes = {"app.worker.make_video": "video", "app.worker.super_resolution": "image", "app.worker.make_video_test": "video"}

+ 1 - 0
backend/app/app/core/celeryconf.py

@@ -0,0 +1 @@
+task_track_started = True

+ 2 - 1
backend/app/app/crud/__init__.py

@@ -1,6 +1,7 @@
 from .crud_user import user
 from .crud_user import user
 from .crud_video import video
 from .crud_video import video
-from .crud_article import artivle
+from .crud_article import article
+from .crud_ser_no import serial_number
 # For a new basic set of CRUD operations you could just do
 # For a new basic set of CRUD operations you could just do
 
 
 # from .base import CRUDBase
 # from .base import CRUDBase

+ 1 - 1
backend/app/app/crud/crud_article.py

@@ -20,4 +20,4 @@ class CRUDArticle(CRUDBase[Article, ArticleCreate, ArticleUpdate]):
         db.refresh(db_obj)
         db.refresh(db_obj)
         return db_obj
         return db_obj
     
     
-artivle = CRUDArticle(Article)
+article = CRUDArticle(Article)

+ 25 - 0
backend/app/app/crud/crud_ser_no.py

@@ -0,0 +1,25 @@
+from typing import List, Optional
+
+from fastapi.encoders import jsonable_encoder
+from sqlalchemy.orm import Session
+
+from app.crud.base import CRUDBase
+from app.models.serial_number import SerialNumber
+from app.schemas.serial_number import SerialNumberCreate, SerialNumberUpdate
+
+from app.utils import random_name
+
+class CRUDSerialNumber(CRUDBase[SerialNumber, SerialNumberCreate, SerialNumberUpdate]):
+    
+    def valid(self, db: Session, *, ser_no:str) -> Optional[SerialNumber]:
+        return db.query(SerialNumber).filter(SerialNumber.code==ser_no).first()
+
+    def available(self, db: Session, *, ser_no:str) -> Optional[SerialNumber]:
+        valid_ser_no = self.valid(db, ser_no=ser_no)
+        if valid_ser_no and not valid_ser_no.is_used:
+            return valid_ser_no
+        else:
+            return None
+        
+    
+serial_number = CRUDSerialNumber(SerialNumber)

+ 3 - 3
backend/app/app/crud/crud_video.py

@@ -35,9 +35,9 @@ class CRUDVideo(CRUDBase[Video, VideoCreate, VideoUpdate]):
       self, db: Session, *, n:int
       self, db: Session, *, n:int
     ) -> bool:
     ) -> bool:
         while True:
         while True:
-            file_name = random_name(n)
-            if not db.query(self.model).filter(Video.stored_file_name==file_name).first():
-              return file_name
+            filename = random_name(n)
+            if not db.query(self.model).filter(Video.stored_filename==filename).first():
+              return filename
 
 
 
 
 video = CRUDVideo(Video)
 video = CRUDVideo(Video)

+ 2 - 0
backend/app/app/db/base.py

@@ -6,3 +6,5 @@ from app.models.video import Video
 from app.models.enum import Progress, Membership
 from app.models.enum import Progress, Membership
 from app.models.character import Character
 from app.models.character import Character
 from app.models.article import Article
 from app.models.article import Article
+from app.models.serial_number import SerialNumber
+from app.models.voice import Voice

+ 3 - 1
backend/app/app/models/__init__.py

@@ -1,4 +1,6 @@
 from .user import User
 from .user import User
 from .video import Video
 from .video import Video
 from .enum import Membership, Progress
 from .enum import Membership, Progress
-from .article import Article
+from .article import Article
+from .serial_number import SerialNumber
+from .voice import Voice

+ 14 - 0
backend/app/app/models/serial_number.py

@@ -0,0 +1,14 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String, DateTime, Boolean
+
+from app.db.base_class import Base
+
+class SerialNumber(Base):
+  __tablename__ = "serial_number"
+  code = Column(String(length=20), primary_key=True)
+  time = Column(Integer, nullable=False)
+  created_datetime = Column(DateTime)
+  is_used = Column(Boolean, nullable=False, default=False)
+  used_datetime = Column(DateTime)
+  owner_id = Column(Integer, ForeignKey("user.id"))

+ 1 - 1
backend/app/app/models/user.py

@@ -11,7 +11,7 @@ if TYPE_CHECKING:
 class User(Base):
 class User(Base):
   id = Column(Integer, primary_key=True, index=True)
   id = Column(Integer, primary_key=True, index=True)
   full_name = Column(String(20), index=True)
   full_name = Column(String(20), index=True)
-  email = Column(String(30), unique=True, index=True, nullable=False)
+  email = Column(String(50), unique=True, index=True, nullable=False)
   hashed_password = Column(String(100), nullable=False)
   hashed_password = Column(String(100), nullable=False)
   membership_status = Column(String(10), 
   membership_status = Column(String(10), 
                       ForeignKey("membership.status", onupdate="CASCADE", ondelete="RESTRICT"), 
                       ForeignKey("membership.status", onupdate="CASCADE", ondelete="RESTRICT"), 

+ 1 - 1
backend/app/app/models/video.py

@@ -13,7 +13,7 @@ if TYPE_CHECKING:
 class Video(Base):
 class Video(Base):
   id = Column(Integer, primary_key=True, index=True)
   id = Column(Integer, primary_key=True, index=True)
   title = Column(String(20), index=True, nullable=False)
   title = Column(String(20), index=True, nullable=False)
-  stored_file_name = Column(String(30), unique=True, nullable=False)
+  stored_filename = Column(String(30), unique=True, nullable=False)
   progress_state = Column(String(10), 
   progress_state = Column(String(10), 
                     ForeignKey("progress.state", ondelete="RESTRICT", onupdate="CASCADE"),
                     ForeignKey("progress.state", ondelete="RESTRICT", onupdate="CASCADE"),
                     default="waiting")
                     default="waiting")

+ 10 - 0
backend/app/app/models/voice.py

@@ -0,0 +1,10 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String, DateTime, Boolean
+
+from app.db.base_class import Base
+
+class Voice(Base):
+  id = Column(Integer, primary_key=True, index=True,)
+  name = Column(String(30))
+  play_id = Column(String(30))

+ 1 - 0
backend/app/app/schemas/__init__.py

@@ -3,3 +3,4 @@ from .user import User, UserCreate, UserInDB, UserUpdate
 from .msg import Msg
 from .msg import Msg
 from .video import Video, VideoCreate, VideoInDB, VideoUpdate
 from .video import Video, VideoCreate, VideoInDB, VideoUpdate
 from .article import ArticleBase, ArticleCreate, ArticleUpdate
 from .article import ArticleBase, ArticleCreate, ArticleUpdate
+from .serial_number import SerialNumberBase, SerialNumberCreate, SerialNumberUpdate

+ 21 - 0
backend/app/app/schemas/serial_number.py

@@ -0,0 +1,21 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+
+class SerialNumberBase(BaseModel):
+    code: Optional[str] = None
+    time: Optional[int] = None
+    created_datetime: Optional[str] = None
+    is_used: Optional[bool] = False
+    used_datetime: Optional[str] = None
+
+# Properties to receive via API on creation
+class SerialNumberCreate(SerialNumberBase):
+    pass
+
+
+# Properties to receive via API on update
+class SerialNumberUpdate(SerialNumberBase):
+    owner_id: int

+ 1 - 0
backend/app/app/schemas/user.py

@@ -19,6 +19,7 @@ class UserCreate(UserBase):
 # Properties to receive via API on update
 # Properties to receive via API on update
 class UserUpdate(UserBase):
 class UserUpdate(UserBase):
     password: Optional[str] = None
     password: Optional[str] = None
+    available_time: Optional[int] = None
 
 
 
 
 class UserInDBBase(UserBase):
 class UserInDBBase(UserBase):

+ 2 - 2
backend/app/app/schemas/video.py

@@ -8,13 +8,13 @@ from fastapi import UploadFile, File
 class VideoBase(BaseModel):
 class VideoBase(BaseModel):
     title: Optional[str] = None
     title: Optional[str] = None
     progress_state: Optional[str] = None
     progress_state: Optional[str] = None
-    stored_file_name: Optional[str] = None
+    stored_filename: Optional[str] = None
 
 
 # Properties to receive on video creation
 # Properties to receive on video creation
 class VideoCreate(VideoBase):
 class VideoCreate(VideoBase):
     title: str
     title: str
     progress_state: str
     progress_state: str
-    stored_file_name: str
+    stored_filename: str
 
 
 # Properties to receive on video update
 # Properties to receive on video update
 class VideoUpdate(VideoBase):
 class VideoUpdate(VideoBase):

+ 46 - 34
backend/app/app/worker.py

@@ -6,6 +6,7 @@ import requests
 from pathlib import Path
 from pathlib import Path
 from urllib.parse import urlparse, urljoin
 from urllib.parse import urlparse, urljoin
 from .aianchor import  make_video_from_zip
 from .aianchor import  make_video_from_zip
+import gc
 #client_sentry = Client(settings.SENTRY_DSN)
 #client_sentry = Client(settings.SENTRY_DSN)
 import dataset
 import dataset
 from app.db.session import SessionLocal
 from app.db.session import SessionLocal
@@ -21,11 +22,18 @@ CELERY_VIDEO_STORAGE = Path("/").joinpath(settings.LOCAL_VIDEO_STORAGE)
 LOCAL_VIDEO_STORAGE = Path("/").joinpath(settings.LOCAL_VIDEO_STORAGE)
 LOCAL_VIDEO_STORAGE = Path("/").joinpath(settings.LOCAL_VIDEO_STORAGE)
 
 
 STORAGE_IP = '192.168.192.252'#os.getenv('STORAGE_IP')
 STORAGE_IP = '192.168.192.252'#os.getenv('STORAGE_IP')
+BACKEND_IP = '172.'
 if not STORAGE_IP:
 if not STORAGE_IP:
     raise Exception
     raise Exception
 
 
 @celery_app.task(acks_late=True, bind=True, track_started=True)
 @celery_app.task(acks_late=True, bind=True, track_started=True)
-def make_video(self, video_id, filename, user_id) -> str:
+def make_video_test(self, title=None, anchor_id=None, lang_id=None)->int:
+   print(title, anchor_id, lang_id)
+   return 0
+
+@celery_app.task(acks_late=True, bind=True, track_started=True)
+def make_video(self, *, video_id, stored_filename=None, user_id=None, anchor_id=None, membership=None, available_time=None, **others) -> int:
+    
     #video_id, zip_filename, user_id = args
     #video_id, zip_filename, user_id = args
     # download 
     # download 
     '''
     '''
@@ -35,51 +43,55 @@ def make_video(self, video_id, filename, user_id) -> str:
         for chunk in r.iter_content(chunk_size=1024):
         for chunk in r.iter_content(chunk_size=1024):
             f.write(chunk)
             f.write(chunk)
     '''
     '''
-    db = SessionLocal()
-    db.execute("SELECT 1")
-    zip_filename = filename + ".zip"
+    # db = SessionLocal()
+    # db.execute("SELECT 1")
+    zip_filename = stored_filename + ".zip"
     r = subprocess.run(["sshpass", "-p", "choozmo9", 
     r = subprocess.run(["sshpass", "-p", "choozmo9", 
                         "scp", "-o", "StrictHostKeyChecking=no", f"root@{STORAGE_IP}:{str(LOCAL_ZIP_STORAGE/zip_filename)}", f"{str(CELERY_ZIP_STORAGE/zip_filename)}"])
                         "scp", "-o", "StrictHostKeyChecking=no", f"root@{STORAGE_IP}:{str(LOCAL_ZIP_STORAGE/zip_filename)}", f"{str(CELERY_ZIP_STORAGE/zip_filename)}"])
     print(f'get from local storage: {r.returncode}')
     print(f'get from local storage: {r.returncode}')
-    print(f"video_id: {video_id}, file name: {filename}")
-    db.execute(f"UPDATE video SET progress_state='processing' where id={video_id}")
-    db.commit()
+    print(f"video_id: {video_id}, file name: {stored_filename}")
+    
+    # db.execute(f"UPDATE video SET progress_state='processing' where id={video_id}")
+    # db.commit()
     # make video
     # make video
+    watermark_path='medias/logo_watermark.jpg'
+    content_time = 0
     try:
     try:
-      content_time = make_video_from_zip(working_dir=CELERY_ZIP_STORAGE,style=Path("app/style/choozmo"),
+      if membership=="infinite":
+         watermark_path=None
+      content_time = make_video_from_zip(working_dir=CELERY_ZIP_STORAGE,style=Path("app/style/choozmo"),  
                                          inputfile=zip_filename,
                                          inputfile=zip_filename,
                                          opening=False, 
                                          opening=False, 
-                                         ending=False,
-                                         watermark_path='medias/logo_watermark.jpg')
+                                         ending=False, 
+                                         watermark_path=watermark_path, 
+                                         available_time=available_time)
     except Exception as e:
     except Exception as e:
-        print(f"error:{e}")
-        db.execute(f"UPDATE video SET progress_state='failed' where id={video_id}")
-        db.commit()
+      print(f"error:{e}")
+      # db.execute(f"UPDATE video SET progress_state='failed' where id={video_id}")
+      # db.commit()
     else:
     else:
-        # 
-        video_filename = filename + ".mp4"
-        r = subprocess.run(["sshpass", "-p", "choozmo9", 
-                            "scp", "-o", "StrictHostKeyChecking=no", f"{str(CELERY_ZIP_STORAGE/'output.mp4')}", f"root@{STORAGE_IP}:{'/var/www/videos/'+video_filename}"])
-        print(f"return to local storage: {r.returncode}")
-        print(f"video_id: {video_id}, file name: {filename}")
+      # 
+      video_filename = stored_filename + ".mp4"
+      r = subprocess.run(["sshpass", "-p", "choozmo9", 
+                          "scp", "-o", "StrictHostKeyChecking=no", f"{str(CELERY_ZIP_STORAGE/'output.mp4')}", f"root@{STORAGE_IP}:{'/var/www/videos/'+video_filename}"])
+      print(f"return to local storage: {r.returncode}")
+      print(f"video_id: {video_id}, file name: {stored_filename}")
 
 
-        db.execute(f"UPDATE video SET progress_state='completed' where id={video_id}")
-        db.commit()
+      # db.execute(f"UPDATE video SET progress_state='completed', length={int(content_time)} where id={video_id}")
+      # db.commit()
 
 
-        gc_ret = gc.collect()
-        print(f"gc_collected: {gc_ret}")
-        
-        return str(int(content_time))
+      
+      gc_num = gc.collect()
+      print(f"gc_num: {gc_num}")
+      return int(content_time)
 
 
 @celery_app.task(acks_late=True, bind=True, track_started=True)
 @celery_app.task(acks_late=True, bind=True, track_started=True)
-def super_resolution(self, dirname:str):
-    print(dirname)
-    re = subprocess.run(["sshpass", "-p", "choozmo9", 
+def super_resolution(self, dirname, filenames):
+   
+    r = subprocess.run(["sshpass", "-p", "choozmo9", 
                         "scp", "-o", "StrictHostKeyChecking=no", "-r", f"root@{STORAGE_IP}:{str(LOCAL_ZIP_STORAGE/dirname)}", f"{str(CELERY_ZIP_STORAGE)}"])
                         "scp", "-o", "StrictHostKeyChecking=no", "-r", f"root@{STORAGE_IP}:{str(LOCAL_ZIP_STORAGE/dirname)}", f"{str(CELERY_ZIP_STORAGE)}"])
-    result_dir = dirname+"_result"
-    re = subprocess.run(["python", "/root/github/GFPGAN/inference_gfpgan.py", "-i", f"{str(CELERY_ZIP_STORAGE/dirname)}", "-o", f"{str(CELERY_ZIP_STORAGE/dirname)}", "-v", "1.4"])
-
-    re = subprocess.run(["sshpass", "-p", "choozmo9", 
+   
+    r = subprocess.run(["python", "/root/github/GFPGAN/inference_gfpgan.py", "-i", f"{str(CELERY_ZIP_STORAGE/dirname)}", "-o", f"{str(CELERY_ZIP_STORAGE/dirname)}", "-v", "1.4", "-s", "2"])
+   
+    r = subprocess.run(["sshpass", "-p", "choozmo9", 
                         "scp", "-o", "StrictHostKeyChecking=no", "-r", f"{str(CELERY_ZIP_STORAGE/dirname)}", f"root@{STORAGE_IP}:{str(LOCAL_ZIP_STORAGE)}"])
                         "scp", "-o", "StrictHostKeyChecking=no", "-r", f"{str(CELERY_ZIP_STORAGE/dirname)}", f"root@{STORAGE_IP}:{str(LOCAL_ZIP_STORAGE)}"])
-    
-    return "complete"

+ 12 - 9
frontend/src/App.vue

@@ -1,11 +1,3 @@
-<template>
-  <v-app>
-    <LoadingView v-if="loggedIn === null" />
-    <RouterView v-else />
-    <NotificationsManager />
-  </v-app>
-</template>
-
 <script setup lang="ts">
 <script setup lang="ts">
 import { RouterView } from "vue-router";
 import { RouterView } from "vue-router";
 import { useMainStore } from "@/stores/main";
 import { useMainStore } from "@/stores/main";
@@ -22,10 +14,21 @@ const loggedIn = mainStoreRef.readIsLoggedIn;
 
 
 //lifecycle
 //lifecycle
 onMounted(() => {
 onMounted(() => {
-  mainStore.checkLoggedIn();
+  let path = location.pathname;
+  if (path !== "/qrcode") {
+    mainStore.checkLoggedIn();
+  }
 });
 });
 </script>
 </script>
 
 
+<template>
+  <v-app>
+    <LoadingView v-if="loggedIn === null" />
+    <RouterView v-else />
+    <NotificationsManager />
+  </v-app>
+</template>
+
 <style lang="scss">
 <style lang="scss">
 :root {
 :root {
   --main-color: #ea5413;
   --main-color: #ea5413;

+ 46 - 18
frontend/src/api.ts

@@ -18,6 +18,29 @@ export const api = {
 
 
     return axios.post(`${apiUrl}/api/v1/login/access-token`, params);
     return axios.post(`${apiUrl}/api/v1/login/access-token`, params);
   },
   },
+  async qrLogInGetToken(username: string, password: string,ser_no: string) {
+    const params = new URLSearchParams();
+    params.append("username", username);
+    params.append("password", password);
+
+    return axios.post(`${apiUrl}/api/v1/login/access-token?add_time_code=${ser_no}`, params);
+  },
+  async googleLogin(username: string) {
+    const params = new URLSearchParams();
+    params.append("username", username);
+    params.append("password", "google");
+
+    return axios.post(`${apiUrl}/api/v1/login/google/access-token`, params);
+  },
+  async qrGoogleLogin(username: string,ser_no: string) {
+    const params = new URLSearchParams();
+    params.append("username", username);
+    params.append("password", "google");
+    return axios.post(`${apiUrl}/api/v1/login/google/access-token?add_time_code=${ser_no}`, params);
+  },
+  async qrAddTime(token: string, code: string) {
+    return axios.get(`${apiUrl}/api/v1/ser_nos/add-time?code=${code}`, authHeaders(token));
+  },
   async getMe(token: string) {
   async getMe(token: string) {
     return axios.get<IUserProfile>(`${apiUrl}/api/v1/users/me`, authHeaders(token));
     return axios.get<IUserProfile>(`${apiUrl}/api/v1/users/me`, authHeaders(token));
   },
   },
@@ -73,8 +96,27 @@ export const api = {
     }
     }
     return axios.post<{ filenames: string[] }>(`${apiUrl}/api/v1/images/sr`, formData, authHeaders(token));
     return axios.post<{ filenames: string[] }>(`${apiUrl}/api/v1/images/sr`, formData, authHeaders(token));
   },
   },
-  async getImage(token: string, data: ImageDownload) {
-    axios({
+  // async getImage(token: string, data: ImageDownload) {
+  //   axios({
+  //     url: `${apiUrl}/api/v1/images/sr?stored_file_name=${data.stored_file_name}&file_name=${data.file_name}`,
+  //     method: 'GET',
+  //     responseType: 'blob',
+  //     headers: {
+  //       'Authorization': `Bearer ${token}`
+  //     },
+  //   }).then((response) => {
+  //     const href = URL.createObjectURL(response.data);
+  //     const link = document.createElement('a');
+  //     link.href = href;
+  //     link.setAttribute('download', `${data.file_name}_hr.png`);
+  //     document.body.appendChild(link);
+  //     link.click();
+  //     document.body.removeChild(link);
+  //     URL.revokeObjectURL(href);
+  //   });
+  // },
+  async downloadImage(token: string, data: ImageDownload) {
+    return axios({
       url: `${apiUrl}/api/v1/images/sr?stored_file_name=${data.stored_file_name}&file_name=${data.file_name}`,
       url: `${apiUrl}/api/v1/images/sr?stored_file_name=${data.stored_file_name}&file_name=${data.file_name}`,
       method: 'GET',
       method: 'GET',
       responseType: 'blob',
       responseType: 'blob',
@@ -82,14 +124,7 @@ export const api = {
         'Authorization': `Bearer ${token}`
         'Authorization': `Bearer ${token}`
       },
       },
     }).then((response) => {
     }).then((response) => {
-      const href = URL.createObjectURL(response.data);
-      const link = document.createElement('a');
-      link.href = href;
-      link.setAttribute('download', `${data.file_name}_hr.png`);
-      document.body.appendChild(link);
-      link.click();
-      document.body.removeChild(link);
-      URL.revokeObjectURL(href);
+      return response;
     });
     });
   },
   },
   async uploadArticle(token: string, article_data: ArticleCreate) {
   async uploadArticle(token: string, article_data: ArticleCreate) {
@@ -101,12 +136,5 @@ export const api = {
   },
   },
   async getVideos(token: string) {
   async getVideos(token: string) {
     return axios.get<Video[]>(`${apiUrl}/api/v1/videos/`, authHeaders(token));
     return axios.get<Video[]>(`${apiUrl}/api/v1/videos/`, authHeaders(token));
-  },
-  async googleLogin(username: string) {
-    const params = new URLSearchParams();
-    params.append("username", username);
-    params.append("password", "google");
-
-    return axios.post(`${apiUrl}/api/v1/login/google/access-token`, params);
-  },
+  }
 };
 };

BIN
frontend/src/assets/img/qrcode/Angela.png


BIN
frontend/src/assets/img/qrcode/aianchor3-title.png


BIN
frontend/src/assets/img/qrcode/aianchor3bg.png


BIN
frontend/src/assets/img/qrcode/angela.webp


BIN
frontend/src/assets/img/qrcode/button1.png


BIN
frontend/src/assets/img/qrcode/button2.png


BIN
frontend/src/assets/img/qrcode/button3.png


BIN
frontend/src/assets/img/qrcode/choozmologo.png


BIN
frontend/src/assets/img/qrcode/icon-19.png


BIN
frontend/src/assets/img/qrcode/icon-20.png


BIN
frontend/src/assets/img/qrcode/line1.png


BIN
frontend/src/assets/img/qrcode/line2.png


BIN
frontend/src/assets/img/qrcode/moichiu.png


BIN
frontend/src/assets/img/qrcode/moichiu.webp


BIN
frontend/src/assets/img/qrcode/startline.png


BIN
frontend/src/assets/img/qrcode/startline1.png


BIN
frontend/src/assets/img/qrcode/儲值卷-06.png


BIN
frontend/src/assets/img/qrcode/儲值卷-07.png


BIN
frontend/src/assets/img/qrcode/儲值卷-13.png


+ 14 - 4
frontend/src/components/Dialog.vue

@@ -17,6 +17,14 @@ const props = defineProps({
   state: {
   state: {
     type: String,
     type: String,
   },
   },
+  icon: {
+    type: String,
+    default: "info"
+  },
+  qrcode: {
+    type: Boolean,
+    default: false,
+  }
 });
 });
 
 
 const emit = defineEmits(["close"]);
 const emit = defineEmits(["close"]);
@@ -35,12 +43,14 @@ function close() {
     <v-card>
     <v-card>
       <v-card-text>
       <v-card-text>
         <section class="d-flex flex-column align-center">
         <section class="d-flex flex-column align-center">
-          <v-icon style="font-size: 70px" icon="info" :color="state" />
-          <p class="mt-3">{{ msg }}</p>
+          <v-icon style="font-size: 70px" :icon="icon" :color="state" />
+          <!-- <p class="mt-3">{{ msg }}</p> -->
+          <p  v-html="msg" class="mt-3 text-center"></p>
         </section>
         </section>
       </v-card-text>
       </v-card-text>
-      <v-card-actions>
-        <v-btn @click="close()" class="mx-auto"> {{ t("close") }}</v-btn>
+      <v-card-actions class="d-flex justify-space-evenly">
+        <v-btn @click="close()" v-show="!qrcode || (state === 'error' && qrcode)">{{ t("close") }}</v-btn>
+        <router-link v-show="(state === 'success' || state === 'error') && qrcode" to="/main/dashboard">返回使用者頁面</router-link>
       </v-card-actions>
       </v-card-actions>
     </v-card>
     </v-card>
   </v-dialog>
   </v-dialog>

+ 1 - 0
frontend/src/components/Navbar.vue

@@ -12,6 +12,7 @@ let lang = reactive([
 let menu = reactive([
 let menu = reactive([
   { title: "login", link: "/login" },
   { title: "login", link: "/login" },
   { title: "register", link: "/signup" },
   { title: "register", link: "/signup" },
+  // { title: "實體卡儲值", link: "/qrcode" },
 ]);
 ]);
 
 
 function setLang(lang: String) {
 function setLang(lang: String) {

+ 1 - 1
frontend/src/env.ts

@@ -5,7 +5,7 @@ let envWsUrl = "";
 
 
 if (env === "production") {
 if (env === "production") {
   envApiUrl = `https://${import.meta.env.VITE_APP_DOMAIN_PROD}`;
   envApiUrl = `https://${import.meta.env.VITE_APP_DOMAIN_PROD}`;
-  envWsUrl = `wss://${import.meta.env.VITE_APP_DOMAIN_DEV}`;
+  envWsUrl = `wss://${import.meta.env.VITE_APP_DOMAIN_PROD}`;
 } else if (env === "staging") {
 } else if (env === "staging") {
   envApiUrl = `https://${import.meta.env.VITE_APP_DOMAIN_STAG}`;
   envApiUrl = `https://${import.meta.env.VITE_APP_DOMAIN_STAG}`;
 } else {
 } else {

+ 3 - 1
frontend/src/interfaces/index.ts

@@ -4,6 +4,7 @@ export interface IUserProfile {
   is_superuser: boolean;
   is_superuser: boolean;
   full_name?: string;
   full_name?: string;
   id: number;
   id: number;
+  available_time: number; 
 }
 }
 
 
 export interface IUserProfileUpdate {
 export interface IUserProfileUpdate {
@@ -43,7 +44,7 @@ export interface MainState {
 export interface Video {
 export interface Video {
   id: number;
   id: number;
   title: string;
   title: string;
-  stored_file_name: string;
+  stored_filename: string;
   progress_state: string;
   progress_state: string;
 }
 }
 
 
@@ -64,6 +65,7 @@ export interface Image {
   stored_file_name: string;
   stored_file_name: string;
   content: string;
   content: string;
   state: string;
   state: string;
+  link: any;
 }
 }
 
 
 export interface ImageDownload {
 export interface ImageDownload {

+ 2 - 0
frontend/src/language/en.json

@@ -1,6 +1,7 @@
 {
 {
     "login" : "Login",
     "login" : "Login",
     "loggedIn" : "Logged in",
     "loggedIn" : "Logged in",
+    "loggedError" : "Incorrect email or password",
     "loginLink" : "Login",
     "loginLink" : "Login",
     "logout": "Logout",
     "logout": "Logout",
     "submit": "Submit",
     "submit": "Submit",
@@ -18,6 +19,7 @@
     "signingUp": "signing up...",
     "signingUp": "signing up...",
     "registerLink": "Register",
     "registerLink": "Register",
     "registerSuccess": "Successfully registered",
     "registerSuccess": "Successfully registered",
+    "registerError": "The user with this email already exists in the system",
     "privacy_term_1": "Registeration implies acception of \n \n \n",
     "privacy_term_1": "Registeration implies acception of \n \n \n",
     "privacy_term_2": "terms of service and privacy policy",
     "privacy_term_2": "terms of service and privacy policy",
     "forgotPsd": "Forgot Password",
     "forgotPsd": "Forgot Password",

+ 2 - 0
frontend/src/language/zh.json

@@ -1,6 +1,7 @@
 {
 {
     "login" : "登入",
     "login" : "登入",
     "loggedIn" : "登入成功",
     "loggedIn" : "登入成功",
+    "loggedError" : "帳號或密碼不正確",
     "loginLink" : "立即登入",
     "loginLink" : "立即登入",
     "logout": "登出",
     "logout": "登出",
     "submit": "送出",
     "submit": "送出",
@@ -18,6 +19,7 @@
     "signingUp": "註冊中...",
     "signingUp": "註冊中...",
     "registerLink": "立即註冊",
     "registerLink": "立即註冊",
     "registerSuccess": "註冊成功",
     "registerSuccess": "註冊成功",
+    "registerError": "此電子郵件地址已被使用",
     "privacy_term_1": "註冊即表示您已閱讀並同意",
     "privacy_term_1": "註冊即表示您已閱讀並同意",
     "privacy_term_2": "服務條款及隱私權政策",
     "privacy_term_2": "服務條款及隱私權政策",
     "forgotPsd": "忘記密碼",
     "forgotPsd": "忘記密碼",

+ 12 - 7
frontend/src/router/index.ts

@@ -6,10 +6,10 @@ const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
   routes: [
     {
     {
-      path: "/",
-      name:'/',
+      path: '/',
+      name: '/',
       component: () => import(/* webpackChunkName: "start" */ '@/views/main/Start.vue'),
       component: () => import(/* webpackChunkName: "start" */ '@/views/main/Start.vue'),
-      children:[
+      children: [
         {
         {
           path: 'login',
           path: 'login',
           name: 'login',
           name: 'login',
@@ -33,6 +33,11 @@ const router = createRouter({
           name: 'reset-password',
           name: 'reset-password',
           component: () => import(/* webpackChunkName: "reset-password" */ '@/views/ResetPassword.vue'),
           component: () => import(/* webpackChunkName: "reset-password" */ '@/views/ResetPassword.vue'),
         },
         },
+        {
+          path: 'qrcode',
+          name: 'qrcode',
+          component: () => import(/* webpackChunkName: "reset-password" */ '@/views/Qrcode.vue'),
+        },
         {
         {
           path: 'main',
           path: 'main',
           name: 'main',
           name: 'main',
@@ -46,22 +51,22 @@ const router = createRouter({
             {
             {
               path: 'make-video',
               path: 'make-video',
               name: 'make-video',
               name: 'make-video',
-              component: () => import ('@/views/main/Upload.vue'),
+              component: () => import('@/views/main/Upload.vue'),
             },
             },
             {
             {
               path: 'make-article',
               path: 'make-article',
               name: 'make-article',
               name: 'make-article',
-              component: () => import ('@/views/main/Article.vue'),
+              component: () => import('@/views/main/Article.vue'),
             },
             },
             {
             {
               path: 'make-image',
               path: 'make-image',
               name: 'make-image',
               name: 'make-image',
-              component: () => import ('@/views/main/Image.vue'),
+              component: () => import('@/views/main/Image.vue'),
             },
             },
             {
             {
               path: 'progress',
               path: 'progress',
               name: 'progress',
               name: 'progress',
-              component: () => import ('@/views/main/Progress.vue'),
+              component: () => import('@/views/main/Progress.vue'),
             },
             },
             {
             {
               path: 'profile',
               path: 'profile',

+ 99 - 43
frontend/src/stores/main.ts

@@ -70,10 +70,56 @@ export const useMainStore = defineStore("MainStoreId", {
           await this.logOut();
           await this.logOut();
         }
         }
       } catch (err) {
       } catch (err) {
+        this.addNotification({ content: i18n.global.t("loggedError"), color: "error" });
         this.setLogInError(true);
         this.setLogInError(true);
         await this.logOut();
         await this.logOut();
       }
       }
     },
     },
+    async qrLogIn(username: string, password: string, ser_no: string) {
+      try {
+        const response = await api.qrLogInGetToken(username, password, ser_no);
+        return response;
+      } catch (error) {
+        console.log('error', error);
+      }
+    },
+    async googleLogin(username: string) {
+      try {
+        const response = await api.googleLogin(username);
+        const token: string = response.data.access_token;
+        if (token) {
+          saveLocalToken(token);
+          this.setToken(token);
+          this.setLoggedIn(true);
+          this.setLogInError(false);
+          await this.getUserProfile();
+          await this.routeLoggedIn();
+          this.addNotification({ content: i18n.global.t("loggedIn"), color: "success" });
+        } else {
+          await this.logOut();
+        }
+      } catch (err) {
+        this.setLogInError(true);
+        await this.logOut();
+      }
+    },
+    async qrGoogleLogin(username: string, ser_no: string) {
+      try {
+        const response = await api.qrGoogleLogin(username, ser_no);
+        return response;
+      } catch (error) {
+        console.log('error', error);
+      }
+    },
+    async qrAddTime(code: string) {
+      try {
+        const response = await api.qrAddTime(this.token, code);
+        return response;
+      } catch (error) {
+        console.log('error',error);
+        return error;
+      }
+    },
     async getUserProfile() {
     async getUserProfile() {
       try {
       try {
         const response = await api.getMe(this.token);
         const response = await api.getMe(this.token);
@@ -128,6 +174,30 @@ export const useMainStore = defineStore("MainStoreId", {
         }
         }
       }
       }
     },
     },
+    async qrCheckLoggedIn() {
+      if (!this.isLoggedIn) {
+        let token = this.token;
+        if (!token) {
+          const localToken = getLocalToken();
+          if (localToken) {
+            this.setToken(localToken);
+            token = localToken;
+          }
+        }
+        if (token) {
+          try {
+            const response = await api.getMe(token);
+            this.setLoggedIn(true);
+            this.setUserProfile(response.data);
+            // router.push("/main/dashboard");
+          } catch (error) {
+            await this.removeLogIn();
+          }
+        } else {
+          await this.removeLogIn();
+        }
+      }
+    },
     async removeLogIn() {
     async removeLogIn() {
       removeLocalToken();
       removeLocalToken();
       this.setToken("");
       this.setToken("");
@@ -188,6 +258,7 @@ export const useMainStore = defineStore("MainStoreId", {
           router.push("/login")
           router.push("/login")
         }, 2000)
         }, 2000)
       } catch (error) {
       } catch (error) {
+        this.addNotification({ content: i18n.global.t("registerError"), color: "error" });
         await this.checkApiError(error);
         await this.checkApiError(error);
       }
       }
     },
     },
@@ -272,7 +343,8 @@ export const useMainStore = defineStore("MainStoreId", {
                 file_name: file[i].name,
                 file_name: file[i].name,
                 stored_file_name: element,
                 stored_file_name: element,
                 content: "sr",
                 content: "sr",
-                state: "subscribe"
+                state: "subscribe",
+                link: ""
               };
               };
               this.addImage(tmpImage);
               this.addImage(tmpImage);
             }
             }
@@ -289,38 +361,42 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
         await mainStore.checkApiError(error);
       }
       }
     },
     },
-    async getImage(data: ImageDownload) {
-      const mainStore = useMainStore();
-      try {
-        const response = (
-          await Promise.all([
-            api.getImage(mainStore.token, data),
-            await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 0)),
-          ])
-        );
-      } catch (error) {
-        await mainStore.checkApiError(error);
-      }
-    },
+    // async getImage(data: ImageDownload) {
+    //   const mainStore = useMainStore();
+    //   try {
+    //     const response = (
+    //       await Promise.all([
+    //         api.getImage(mainStore.token, data),
+    //         await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 0)),
+    //       ])
+    //     );
+    //   } catch (error) {
+    //     await mainStore.checkApiError(error);
+    //   }
+    // },
     addImage(payload: Image) {
     addImage(payload: Image) {
       this.images.push(payload);
       this.images.push(payload);
     },
     },
-    finishImage(payload: string) {
+    async finishImage(payload: ImageDownload) {
       const WS = new WebSocket(`${wsUrl}/api/v1/images/sr`);
       const WS = new WebSocket(`${wsUrl}/api/v1/images/sr`);
       let image = this.images.filter(e => {
       let image = this.images.filter(e => {
-        return payload.includes(e.stored_file_name)
+        return payload.stored_file_name.includes(e.stored_file_name)
       });
       });
-
-      if (image) {
-        image.map(e => {
-          e.state = "completed";
-        })
+      const mainStore = useMainStore();
+      try {
+        const response = await api.downloadImage(mainStore.token, payload);
+        if (image) {
+          image.map(e => {
+            e.state = "completed";
+            e.link = response.data;
+          })
+        }
+      } catch (error) {
+        await mainStore.checkApiError(error);
       }
       }
 
 
       // 全部完成後回傳 WebSocket
       // 全部完成後回傳 WebSocket
       let processing = this.images.find(e => e.state !== "completed");
       let processing = this.images.find(e => e.state !== "completed");
-      console.log('processing', processing);
-
       if (!processing) {
       if (!processing) {
         setTimeout(() => {
         setTimeout(() => {
           // WS.close();
           // WS.close();
@@ -361,26 +437,6 @@ export const useMainStore = defineStore("MainStoreId", {
       } catch (error) {
       } catch (error) {
         await mainStore.checkApiError(error);
         await mainStore.checkApiError(error);
       }
       }
-    },
-    async googleLogin(username: string) {
-      try {
-        const response = await api.googleLogin(username);
-        const token: string = response.data.access_token;
-        if (token) {
-          saveLocalToken(token);
-          this.setToken(token);
-          this.setLoggedIn(true);
-          this.setLogInError(false);
-          await this.getUserProfile();
-          await this.routeLoggedIn();
-          this.addNotification({ content: i18n.global.t("loggedIn"), color: "success" });
-        } else {
-          await this.logOut();
-        }
-      } catch (err) {
-        this.setLogInError(true);
-        await this.logOut();
-      }
     }
     }
   }
   }
 });
 });

+ 1 - 0
frontend/src/views/Login.vue

@@ -111,6 +111,7 @@ onMounted(() => {
             :label="$t('password')"
             :label="$t('password')"
             hint="4-12 位數密碼"
             hint="4-12 位數密碼"
             @click:append="showPassword = !showPassword"
             @click:append="showPassword = !showPassword"
+            @keyup.enter="submit"
             required
             required
           ></v-text-field>
           ></v-text-field>
 
 

+ 766 - 0
frontend/src/views/Qrcode.vue

@@ -0,0 +1,766 @@
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { useMainStore } from "@/stores/main";
+import { useDisplay } from "vuetify";
+import { useRoute } from "vue-router";
+import { useI18n } from "vue-i18n";
+import { googleTokenLogin, decodeCredential } from "vue3-google-login";
+import type { CallbackTypes } from "vue3-google-login";
+// import Navbar from "@/components/Navbar.vue";
+import Dialog from "@/components/Dialog.vue";
+
+const mainStore = useMainStore();
+
+// variable
+const { t } = useI18n();
+const route = useRoute();
+const { name } = useDisplay();
+const email = ref("");
+const password = ref("");
+let ser_no: any = ref("");
+let showPassword = ref(false);
+let loginState = ref(false);
+let loading = ref(false);
+let dialog = reactive({
+  msg: "",
+  state: "",
+  show: false,
+  icon: "check_circle",
+});
+
+// getter
+const width = computed(() => {
+  switch (name.value) {
+    case "xs":
+      return 12;
+    case "sm":
+      return 8;
+  }
+});
+
+function setDialog(status: Boolean, msg: String = "") {
+  if (status) {
+    dialog.show = true;
+    dialog.state = "success";
+    dialog.msg =
+      "儲值成功!<br/>已獲得價值 1000 元的 120 秒影片製作時間<br/>(儲值成功後即可登入電腦版進行影片製作)";
+    dialog.icon = "check_circle";
+  } else {
+    dialog.show = true;
+    dialog.state = "error";
+    dialog.msg = `${msg}`;
+    dialog.icon = "highlight_off";
+  }
+}
+
+// action
+async function submit() {
+  loading.value = true;
+  if (email.value === "" || password.value === "") {
+    loading.value = false;
+    return;
+  }
+
+  let response = await mainStore.qrLogIn(
+    email.value,
+    password.value,
+    ser_no.value
+  );
+
+  loading.value = false;
+
+  if (response?.data.time_added === -1) {
+    setDialog(false, "此序號無效");
+  } else if (response?.status === 200) {
+    setTimeout(() => {
+      setDialog(true);
+    }, 500);
+  }
+}
+
+// lifecycle
+onMounted(() => {
+  console.log("onMounted");
+  if (route.query["add_time_code"]) {
+    ser_no.value = route.query["add_time_code"];
+  }
+  mainStore.qrCheckLoggedIn();
+  if (mainStore.token) {
+    checkCode("");
+  } else {
+    loginState.value = false;
+  }
+
+  async function checkCode(method: string = "") {
+    loading.value = true;
+    let response: any = await mainStore.qrAddTime(ser_no.value);
+    loading.value = false;
+    if (response.status === 200) {
+      loginState.value = true;
+      setDialog(true);
+    } else if (response.response.status === 400) {
+      setDialog(
+        false,
+        "此序號已被使用 <br> This serial number is already used"
+      );
+    }
+  }
+
+  // if (route.params.ser_no) {
+  //   ser_no.value = route.params.ser_no;
+  //   console.log("ser_no.value", ser_no.value);
+  // }
+});
+
+const callback: CallbackTypes.CredentialCallback = async (response: any) => {
+  loading.value = true;
+  const userData: any = decodeCredential(response.credential);
+  let res = await mainStore.qrGoogleLogin(userData.email, ser_no.value);
+  loading.value = false;
+  if (res?.data.time_added === -1) {
+    setDialog(false, "此序號無效");
+  } else if (res?.status === 200) {
+    setTimeout(() => {
+      setDialog(true);
+    }, 500);
+  }
+};
+</script>
+
+<template>
+  <!-- <Navbar /> -->
+  <v-container fluid class="pa-0 overflow-hidden">
+    <div class="ai_anchor3_content">
+      <div class="ai_anchor3_content_box">
+        <img
+          class="ai_anchor3_content_title img-fluid"
+          src="../assets/img/qrcode/aianchor3-title.png"
+          alt=""
+        />
+        <h1>集仕多股份有限公司</h1>
+        <h1>AI 主播&ensp;儲值禮物卡</h1>
+      </div>
+      <div class="ai_anchor3_content_start">
+        <!-- <h1>開放倒數</h1> -->
+        <img
+          class="img-fluid"
+          src="../assets/img/qrcode/startline.png"
+          alt=""
+        />
+      </div>
+
+      <v-row
+        align="center"
+        justify="center"
+        no-gutters
+        class="overflow-hidden mx-auto login-form"
+        v-if="!loginState"
+      >
+        <v-col cols="12" class="px-6 my-8 my-md-0">
+          <div class="form-title">
+            <h3>登入後即可獲得儲值金</h3>
+            <span></span>
+          </div>
+          <v-form ref="form" lazy-validation>
+            <v-text-field
+              v-model="email"
+              name="email"
+              prepend-icon="person"
+              :rules="[(v) => !!v || '請輸入您的帳號']"
+              :label="$t('emailAddress')"
+              required
+            >
+            </v-text-field>
+
+            <v-text-field
+              v-model="password"
+              name="password"
+              id="password"
+              prepend-icon="key"
+              :append-icon="showPassword ? 'visibility' : 'visibility_off'"
+              :rules="[(v) => !!v || '請輸入您的密碼']"
+              :type="showPassword ? 'text' : 'password'"
+              :label="$t('password')"
+              hint="4-12 位數密碼"
+              @click:append="showPassword = !showPassword"
+              @keyup.enter="submit"
+              required
+            >
+            </v-text-field>
+
+            <!-- <p class="text-center">
+              {{ t("haventAccount") }}
+              <router-link to="/signup">{{ t("register") }}</router-link> /
+              <router-link to="/recover-password">{{
+                t("forgotPsd")
+              }}</router-link>
+            </p> -->
+
+            <div class="d-flex flex-column">
+              <v-btn rounded="pill" @click.prevent="submit" class="login-btn">
+                {{ t("loginLink") }}
+              </v-btn>
+
+              <section class="line">
+                <p class="d-none d-sm-block">
+                  沒有帳號嗎?使用 Google 快速註冊
+                </p>
+                <p class="d-block d-sm-none">
+                  沒有帳號嗎? <br />
+                  使用 Google 快速註冊
+                </p>
+                <!-- <span></span> -->
+              </section>
+
+              <div class="mx-auto mt-5" style="max-width: 235px">
+                <GoogleLogin
+                  :callback="callback"
+                  prompt
+                  popup-type="TOKEN"
+                  class="w-100"
+                />
+              </div>
+            </div>
+          </v-form>
+        </v-col>
+      </v-row>
+
+      <div class="ai_anchor_moichiu left-70" style="background: #67b5b5">
+        <v-row align="center" no-gutters class="px-0 mx-0">
+          <v-col cols="12" sm="4">
+            <div class="line1">
+              <img
+                class="img-fluid anchor_moichiu"
+                src="../assets/img/qrcode/moichiu.webp"
+                alt=""
+              />
+              <img
+                class="ai_anchor_line1"
+                src="../assets/img/qrcode/line1.png"
+                alt=""
+              />
+            </div>
+            <div class="anchor_name">
+              <img
+                class="img-fluid"
+                src="../assets/img/qrcode/moichiu.png"
+                alt=""
+              />
+            </div>
+          </v-col>
+          <v-col :cols="width">
+            <div class="ai_anchor_moichiu_text">
+              <p>To. 親愛的 VIP</p>
+              <p>我知道您們已經準備好,開始使用 AI 主播的系統了</p>
+              <p>儲值金還在來的路上</p>
+              <!-- <p>在此之前,系統目前測試中</p> -->
+              <p>預計 2023/04/06 後開始正式上線!</p>
+            </div>
+          </v-col>
+        </v-row>
+      </div>
+      <!-- <div class="mt-100">
+        <div class="ai_anchor_moichiu" style="background: #ce96c1">
+          <v-row align="center" no-gutters class="px-0 mx-0">
+            <v-col :cols="width">
+              <div class="ai_anchor_moichiu_text">
+                <p>如果有任何不清楚的,歡迎與我們聯繫</p>
+                <p>AI 主播系統裡會有使用教學</p>
+                <p>現在即可開始使用</p>
+                <p>先登入系統,看看製作影片還需要哪些東西吧!</p>
+                <p>感謝您的耐心等候</p>
+              </div>
+            </v-col>
+            <v-col cols="4">
+              <div class="line2">
+                <img
+                  class="img-fluid anchor_angela"
+                  src="../assets/img/qrcode/angela.webp"
+                  alt=""
+                />
+                <img
+                  class="ai_anchor_line2"
+                  src="../assets/img/qrcode/line2.png"
+                  alt=""
+                />
+              </div>
+
+              <div class="anchor_name2">
+                <img
+                  class="img-fluid"
+                  src="../assets/img/qrcode/Angela.png"
+                  alt=""
+                />
+              </div>
+            </v-col>
+          </v-row>
+        </div>
+      </div> -->
+
+      <div class="progress-item text-center">
+        <v-progress-circular
+          indeterminate
+          color="primary"
+          :size="50"
+          v-if="loading"
+        ></v-progress-circular>
+      </div>
+
+      <div class="CTA_Button_div text-center">
+        <button type="button" class="CTA_Button">
+          <!-- 點我開始製作 AI 主播!<br />Log In -->
+          AI 三代主播系統 <br />
+          於 2023/04/06 正式上線!
+        </button>
+      </div>
+
+      <div class="CTA_box">
+        <a href="https://ai.choozmo.com/ai-presenter/info/" target="_blank">
+          <div class="cta-content">看更多<br />官網介紹</div>
+        </a>
+
+        <a href="https://ai.choozmo.com/contact/service/" target="_blank">
+          <div class="cta-content">
+            聯絡我們
+            <img class="icon20" src="../assets/img/qrcode/icon-19.png" alt="" />
+          </div>
+        </a>
+
+        <a href="https://line.me/R/ti/p/@choozmo?from=page" target="_blank">
+          <div class="cta-content">
+            關注我們
+            <img class="icon20" src="../assets/img/qrcode/icon-20.png" alt="" />
+          </div>
+        </a>
+      </div>
+
+      <div class="logo_box">
+        <img
+          class="choozmologo"
+          src="../assets/img/qrcode/choozmologo.png"
+          alt=""
+        />
+      </div>
+    </div>
+    <Dialog
+      :msg="dialog.msg"
+      :state="dialog.state"
+      :dialog="dialog.show"
+      :icon="dialog.icon"
+      :qrcode="true"
+      @close="dialog.show = false"
+    ></Dialog>
+  </v-container>
+</template>
+
+<style lang="scss" scoped>
+.ai_anchor3_content {
+  background-image: url("../assets/img/qrcode/aianchor3bg.png");
+  width: 100%;
+  background-size: cover;
+  background-repeat: no-repeat;
+}
+.login-form {
+  color: #fff;
+  margin-bottom: 100px;
+  .line {
+    margin-top: 50px;
+    display: flex;
+    justify-content: center;
+    position: relative;
+    p {
+      position: relative;
+      z-index: 1;
+      color: #fff;
+      letter-spacing: 1px;
+      font-size: 24px;
+      font-weight: bold;
+      text-align: center;
+      @media (max-width: 600px) {
+        font-size: 18px;
+      }
+      &::after,
+      &::before {
+        content: "";
+        display: block;
+        height: 1px;
+        width: 50px;
+        background: #fff;
+        position: absolute;
+        bottom: 18px;
+        @media (max-width: 600px) {
+          bottom: 20px;
+        }
+      }
+      &::after {
+        left: -55px;
+      }
+      &::before {
+        right: -55px;
+      }
+    }
+  }
+  .login-btn {
+    color: #fff;
+    background: #4f4fa0;
+  }
+
+  .form-title span {
+    background: #fff !important;
+  }
+}
+.logo_box {
+  text-align: end;
+  overflow: hidden;
+}
+.choozmologo {
+  width: 500px;
+  margin-right: -50px;
+  margin-bottom: -80px;
+  @media (max-width: 767px) {
+    width: 80vw;
+  }
+  @media (max-width: 376px) {
+    width: 85vw;
+  }
+}
+.cta-content {
+  width: 200px;
+  height: 130px;
+  margin: 0 20px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  border: 3px solid #fff;
+  padding: 30px 20px;
+  color: #fff;
+  border-radius: 10px;
+  text-align: center;
+  font-size: 24px;
+  @media (max-width: 767px) {
+    width: 100%;
+    margin: 5vh auto 0;
+  }
+  @media (max-width: 376px) {
+    width: 187px;
+  }
+}
+.icon20 {
+  width: 50px;
+}
+.CTA_box {
+  width: 50%;
+  margin: 100px auto;
+  display: flex;
+  justify-content: space-evenly;
+  @media (max-width: 1200px) {
+    width: 70%;
+  }
+  @media (max-width: 767px) {
+    flex-direction: column;
+  }
+}
+.CTA_Button_div {
+  margin-top: 100px;
+}
+.CTA_Button {
+  width: 400px;
+  height: 120px;
+  border-radius: 50px;
+  background: #4f4fa0;
+  transition: all 300ms ease-in-out;
+  box-shadow: 1px 15px #2e3287;
+  color: #fff;
+  border: none;
+  padding: 0px 30px;
+  font-size: 24px;
+  position: relative;
+  bottom: 0px;
+  left: 0px;
+  transition: all 300ms ease-in-out;
+  font-weight: 600;
+  letter-spacing: 2px;
+  &:hover {
+    bottom: -10px;
+    box-shadow: none;
+  }
+  @media (max-width: 991px) {
+    width: 80%;
+  }
+
+  @media (max-width: 767px) {
+    width: 80%;
+    margin-top: 20vh;
+  }
+  @media (max-width: 575px) {
+    margin-top: 25vh;
+    font-size: 20px;
+  }
+}
+.ai_anchor3_content_box {
+  width: 50vw;
+  margin: 0 auto;
+  padding-top: 150px;
+  text-align: center;
+  color: #fff;
+
+  h1 {
+    font-size: 2rem;
+  }
+  @media (max-width: 991px) {
+    width: 80vw;
+  }
+  @media (max-width: 767px) {
+    width: 90vw;
+  }
+}
+.ai_anchor_moichiu_text {
+  @media (max-width: 767px) {
+    height: 300px;
+    padding: 0;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+}
+
+.ai_anchor3_content_start {
+  padding-top: 50px;
+  width: 93%;
+  margin: 0 auto;
+  text-align: center;
+  color: #fff;
+  margin-bottom: 15px;
+  h1 {
+    margin-bottom: -60px;
+  }
+}
+.left-70 {
+  left: 70px;
+  @media (max-width: 991px) {
+    left: 0px;
+  }
+}
+.mt-100 {
+  margin-top: 100px;
+
+  @media (max-width: 767px) {
+    margin-top: 25vh;
+  }
+  @media (max-width: 400px) {
+    margin-top: 35vh;
+  }
+}
+.ai_anchor_moichiu {
+  width: 75%;
+  margin: auto;
+  border-radius: 1rem;
+  padding: 50px 10px;
+  position: relative;
+  @media (max-width: 991px) {
+    width: 80%;
+    padding: 40px 10px;
+  }
+  @media (max-width: 767px) {
+    padding: 20px 10px;
+  }
+  @media (max-width: 767px) {
+    width: 90%;
+  }
+  .anchor_moichiu {
+    margin-left: -60px;
+
+    @media (max-width: 767px) {
+      margin-bottom: 0px;
+      margin-top: 0;
+      position: absolute;
+      width: 50vw;
+      left: 53px;
+      bottom: -134px;
+    }
+  }
+
+  .anchor_moichiu,
+  .anchor_angela {
+    width: 350px;
+    max-width: unset;
+    @media (max-width: 1200px) {
+      width: 300px;
+    }
+    @media (max-width: 991px) {
+      width: 275px;
+    }
+  }
+  .anchor_angela {
+    @media (max-width: 767px) {
+      margin-bottom: 0px;
+      margin-top: 0;
+      position: absolute;
+      right: -60vw;
+      bottom: -103px;
+    }
+  }
+  .anchor_name {
+    position: absolute;
+    bottom: -20px;
+    left: -8px;
+    width: 80px;
+
+    @media (max-width: 991px) {
+      width: 50px;
+      left: -8px;
+      bottom: -20px;
+    }
+    @media (max-width: 767px) {
+      left: 250px;
+      bottom: -170px;
+    }
+
+    img {
+      @media (max-width: 1200px) {
+        max-width: unset;
+        width: 70px;
+      }
+      @media (max-width: 991px) {
+        max-width: unset;
+        width: 65px;
+      }
+    }
+  }
+
+  .anchor_name2 {
+    position: absolute;
+    bottom: -5px;
+    right: 0;
+    width: 100px;
+    @media (max-width: 991px) {
+      bottom: -8px;
+      right: -3px;
+      width: 80px;
+    }
+    @media (max-width: 767px) {
+      bottom: -27vw;
+      right: 280px;
+      width: 100px;
+    }
+    @media (max-width: 575px) {
+      bottom: -32vw;
+      right: 270px;
+      width: 70px;
+    }
+    @media (max-width: 414px) {
+      bottom: -40vw;
+      right: 285px;
+    }
+    p {
+      color: #fff !important;
+      font-size: 14px;
+      margin-bottom: 0rem;
+    }
+    img {
+      width: 100px;
+      @media (max-width: 991px) {
+        width: 80px;
+      }
+      @media (max-width: 767px) {
+        width: 100px;
+      }
+    }
+  }
+  .line1,
+  .line2 {
+    position: relative;
+  }
+
+  .line1 {
+    @media (max-width: 767px) {
+      bottom: -360px;
+    }
+  }
+
+  .line2 {
+    @media (max-width: 991px) {
+      margin-left: 30px;
+    }
+    @media (max-width: 767px) {
+      bottom: -120px;
+    }
+  }
+  .ai_anchor_line1 {
+    max-width: 1500px;
+    position: absolute;
+    bottom: 0px;
+    left: 165px;
+    object-fit: cover;
+    @media (max-width: 1200px) {
+      max-width: 1000px;
+      left: 140px;
+    }
+    @media (max-width: 991px) {
+      width: 85vw;
+      left: 123px;
+    }
+    @media (max-width: 767px) {
+      left: 175px;
+      bottom: -138px;
+    }
+    @media (max-width: 575px) {
+      left: 168px;
+    }
+  }
+  .ai_anchor_line2 {
+    width: 1300px;
+    height: 290px;
+    position: absolute;
+    bottom: 6px;
+    left: -1238px;
+
+    @media (max-width: 1200px) {
+      width: 830px;
+      height: auto;
+      bottom: -5px;
+      left: -775px;
+    }
+
+    @media (max-width: 991px) {
+      width: 700px;
+      height: auto;
+      bottom: -5px;
+      left: -665px;
+    }
+    @media (max-width: 767px) {
+      width: 100vw;
+      bottom: -15vw;
+      left: -345px;
+    }
+    @media (max-width: 575px) {
+      left: -333px;
+    }
+  }
+  p {
+    font-weight: 900;
+    letter-spacing: 3px;
+    text-align: center;
+    margin-bottom: 0.3rem;
+    font-size: 22px;
+
+    @media (max-width: 991px) {
+      font-size: 20px;
+    }
+    @media (max-width: 767px) {
+      font-size: 16px;
+    }
+  }
+}
+.img-fluid {
+  max-width: 100%;
+  height: auto;
+}
+.progress-item {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+</style>

+ 9 - 3
frontend/src/views/main/Dashboard.vue

@@ -11,6 +11,8 @@ const userProfile = mainStoreRef.readUserProfile;
 
 
 const greetedUser = computed(() => {
 const greetedUser = computed(() => {
   const userProfile = mainStoreRef.readUserProfile;
   const userProfile = mainStoreRef.readUserProfile;
+  console.log("userProfile", userProfile);
+
   if (userProfile.value) {
   if (userProfile.value) {
     if (userProfile.value!.full_name) {
     if (userProfile.value!.full_name) {
       return userProfile.value!.full_name;
       return userProfile.value!.full_name;
@@ -71,7 +73,7 @@ const greetedUser = computed(() => {
             <h3>已使用秒數</h3>
             <h3>已使用秒數</h3>
           </v-card-title>
           </v-card-title>
           <v-card-text class="mt-3">
           <v-card-text class="mt-3">
-            <strong>50</strong><small>秒</small>
+            <strong>0</strong><small>秒</small>
           </v-card-text>
           </v-card-text>
           <v-divider></v-divider>
           <v-divider></v-divider>
           <v-card-actions>
           <v-card-actions>
@@ -99,8 +101,12 @@ const greetedUser = computed(() => {
           <v-card-title primary-title>
           <v-card-title primary-title>
             <h3>可使用秒數</h3>
             <h3>可使用秒數</h3>
           </v-card-title>
           </v-card-title>
-          <v-card-text class="mt-3">
-            <strong>300</strong><small>秒</small>
+          <v-card-text
+            v-if="userProfile"
+            class="mt-3"
+          >
+            <strong>{{ userProfile.available_time }}</strong
+            ><small>秒</small>
           </v-card-text>
           </v-card-text>
           <v-divider></v-divider>
           <v-divider></v-divider>
           <v-card-actions>
           <v-card-actions>

+ 32 - 7
frontend/src/views/main/Image.vue

@@ -68,18 +68,43 @@ onMounted(() => {
   // webSocket
   // webSocket
   WS.onmessage = function (e) {
   WS.onmessage = function (e) {
     setTimeout(() => {
     setTimeout(() => {
-      mainStore.finishImage(e.data);
+      let image: ImageDownload = {
+        file_name: "",
+        stored_file_name: "",
+      };
+
+      mainStore.images.map((item) => {
+        if (item.stored_file_name === e.data) {
+          image.file_name = item.file_name;
+          image.stored_file_name = item.stored_file_name;
+          mainStore.finishImage(image);
+        }
+      });
+      
     }, 1000);
     }, 1000);
   };
   };
 });
 });
 
 
-async function downloadImg(name: string, id: string) {
-  const data: ImageDownload = {
-    file_name: name.split(".")[0],
-    stored_file_name: id,
-  };
+async function downloadImg(file_name: string, stored_file_name: string) {
+  mainStore.images.map((item) => {
+    if (item.stored_file_name === stored_file_name) {
+      // 生成下載連結
+      const href = URL.createObjectURL(item.link);
+      const link = document.createElement("a");
+      link.href = href;
+      link.setAttribute("download", `${file_name}_hr.png`);
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      URL.revokeObjectURL(href);
+    }
+  });
+  // const data: ImageDownload = {
+  //   file_name: file_name.split(".")[0],
+  //   stored_file_name: stored_file_name,
+  // };
 
 
-  await mainStore.getImage(data);
+  // await mainStore.getImage(data);
 }
 }
 
 
 const headers = [
 const headers = [

+ 41 - 37
frontend/src/views/main/Start.vue

@@ -1,47 +1,51 @@
-
 <template>
 <template>
   <router-view></router-view>
   <router-view></router-view>
 </template>
 </template>
-  
+
 <script lang="ts">
 <script lang="ts">
-  import { onBeforeRouteUpdate } from 'vue-router'
-  import type { RouteLocationNormalized , NavigationGuardNext} from 'vue-router';
-  import { useMainStore } from '@/stores/main';
-  import { storeToRefs } from "pinia";
-  export default {
-    setup(){
-      onBeforeRouteUpdate((to, from, next) =>  {
-        startRouteGuard(to, from, next);
-      });
-    },
-    beforeRouteEnter(to, from, next){
+import { onBeforeRouteUpdate } from "vue-router";
+import type { RouteLocationNormalized, NavigationGuardNext } from "vue-router";
+import { useMainStore } from "@/stores/main";
+import { storeToRefs } from "pinia";
+export default {
+  setup() {
+    onBeforeRouteUpdate((to, from, next) => {
       startRouteGuard(to, from, next);
       startRouteGuard(to, from, next);
-    }
+    });
+  },
+  beforeRouteEnter(to, from, next) {
+    startRouteGuard(to, from, next);
+  },
+};
 
 
-  } 
-  // variable
+//function
+const startRouteGuard = async (
+  to: RouteLocationNormalized,
+  from: RouteLocationNormalized,
+  next: NavigationGuardNext
+) => {
+  const mainStore = useMainStore();
+  const mainStoreRef = storeToRefs(mainStore);
 
 
-  // excute in setup
-  
-  //lifecycle
-  
-  //function
-  const startRouteGuard = async (to:RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
-    const mainStore = useMainStore();
-    const mainStoreRef = storeToRefs(mainStore);
+  if (to.path === "/qrcode") {
+    next();
+    mainStore.qrCheckLoggedIn();
+  } else {
     mainStore.checkLoggedIn();
     mainStore.checkLoggedIn();
-    if (mainStoreRef.readIsLoggedIn.value) {
-      if (to.path === '/login' || to.path === '/') {
-        next('/main/dashboard');
-      } else {
-        next();
-      }
-    } else if (mainStoreRef.readIsLoggedIn.value === false) {
-      if (to.path === '/' || (to.path as string).startsWith('/main')) {
-        next('/login');
-      } else {
-        next();
-      }
+  }
+
+  if (mainStoreRef.readIsLoggedIn.value) {
+    if (to.path === "/login" || to.path === "/") {
+      next("/main/dashboard");
+    } else {
+      next();
+    }
+  } else if (mainStoreRef.readIsLoggedIn.value === false) {
+    if (to.path === "/" || (to.path as string).startsWith("/main")) {
+      next("/login");
+    } else {
+      next();
     }
     }
-  };
+  }
+};
 </script>
 </script>