filechooser.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. package views
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "github.com/qor5/admin/presets"
  6. "mime/multipart"
  7. "strconv"
  8. "strings"
  9. "github.com/qor5/admin/media"
  10. "github.com/qor5/admin/media/media_library"
  11. . "github.com/qor5/ui/vuetify"
  12. "github.com/qor5/web"
  13. "github.com/qor5/x/i18n"
  14. h "github.com/theplant/htmlgo"
  15. "gorm.io/gorm"
  16. )
  17. func fileChooser(db *gorm.DB) web.EventFunc {
  18. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  19. msgr := i18n.MustGetModuleMessages(ctx.R, I18nMediaLibraryKey, Messages_en_US).(*Messages)
  20. field := ctx.R.FormValue("field")
  21. cfg := stringToCfg(ctx.R.FormValue("cfg"))
  22. portalName := mainPortalName(field)
  23. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  24. Name: portalName,
  25. Body: VDialog(
  26. VCard(
  27. VToolbar(
  28. VBtn("").
  29. Icon(true).
  30. Dark(true).
  31. Attr("@click", "vars.showFileChooser = false").
  32. Children(
  33. VIcon("close"),
  34. ),
  35. VToolbarTitle(msgr.ChooseAFile),
  36. VSpacer(),
  37. VLayout(
  38. VTextField().
  39. SoloInverted(true).
  40. PrependIcon("search").
  41. Label(msgr.Search).
  42. Flat(true).
  43. Clearable(true).
  44. HideDetails(true).
  45. Value("").
  46. Attr("@keyup.enter", web.Plaid().
  47. EventFunc(imageSearchEvent).
  48. Query("field", field).
  49. FieldValue("cfg", h.JSONString(cfg)).
  50. FieldValue(searchKeywordName(field), web.Var("$event")).
  51. Go()),
  52. ).AlignCenter(true).Attr("style", "max-width: 650px"),
  53. ).Color("primary").
  54. // MaxHeight(64).
  55. Flat(true).
  56. Dark(true),
  57. web.Portal().Name(deleteConfirmPortalName(field)),
  58. web.Portal(
  59. fileChooserDialogContent(db, field, ctx, cfg),
  60. ).Name(dialogContentPortalName(field)),
  61. ).Tile(true),
  62. ).
  63. Fullscreen(true).
  64. // HideOverlay(true).
  65. Transition("dialog-bottom-transition").
  66. // Scrollable(true).
  67. Attr("v-model", "vars.showFileChooser"),
  68. })
  69. r.VarsScript = `setTimeout(function(){ vars.showFileChooser = true }, 100)`
  70. return
  71. }
  72. }
  73. func fileChooserDialogContent(db *gorm.DB, field string, ctx *web.EventContext, cfg *media_library.MediaBoxConfig) h.HTMLComponent {
  74. msgr := i18n.MustGetModuleMessages(ctx.R, I18nMediaLibraryKey, Messages_en_US).(*Messages)
  75. keyword := ctx.R.FormValue(searchKeywordName(field))
  76. type selectItem struct {
  77. Text string
  78. Value string
  79. }
  80. const (
  81. orderByKey = "order_by"
  82. orderByCreatedAt = "created_at"
  83. orderByCreatedAtDESC = "created_at_desc"
  84. )
  85. orderBy := ctx.R.URL.Query().Get(orderByKey)
  86. var files []*media_library.MediaLibrary
  87. wh := db.Model(&media_library.MediaLibrary{})
  88. switch orderBy {
  89. case orderByCreatedAt:
  90. wh = wh.Order("created_at")
  91. default:
  92. orderBy = orderByCreatedAtDESC
  93. wh = wh.Order("created_at DESC")
  94. }
  95. currentPageInt, _ := strconv.Atoi(ctx.R.FormValue(currentPageName(field)))
  96. if currentPageInt == 0 {
  97. currentPageInt = 1
  98. }
  99. if len(cfg.Sizes) > 0 {
  100. cfg.AllowType = media_library.ALLOW_TYPE_IMAGE
  101. }
  102. if len(cfg.AllowType) > 0 {
  103. wh = wh.Where("selected_type = ?", cfg.AllowType)
  104. }
  105. if len(keyword) > 0 {
  106. wh = wh.Where("file ILIKE ?", fmt.Sprintf("%%%s%%", keyword))
  107. }
  108. var count int64
  109. err := wh.Count(&count).Error
  110. if err != nil {
  111. panic(err)
  112. }
  113. perPage := MediaLibraryPerPage
  114. pagesCount := int(count/int64(perPage) + 1)
  115. if count%int64(perPage) == 0 {
  116. pagesCount--
  117. }
  118. wh = wh.Limit(perPage).Offset((currentPageInt - 1) * perPage)
  119. err = wh.Find(&files).Error
  120. if err != nil {
  121. panic(err)
  122. }
  123. fileAccept := "*/*"
  124. if cfg.AllowType == media_library.ALLOW_TYPE_IMAGE {
  125. fileAccept = "image/*"
  126. }
  127. row := VRow(
  128. h.If(uploadIsAllowed(ctx.R) == nil,
  129. VCol(
  130. h.Label("").Children(
  131. VCard(
  132. VCardTitle(h.Text(msgr.UploadFiles)),
  133. VIcon("backup").XLarge(true),
  134. h.Input("").
  135. Attr("accept", fileAccept).
  136. Type("file").
  137. Attr("multiple", true).
  138. Style("display:none").
  139. Attr("@change",
  140. web.Plaid().
  141. BeforeScript("locals.fileChooserUploadingFiles = $event.target.files").
  142. FieldValue("NewFiles", web.Var("$event")).
  143. EventFunc(uploadFileEvent).
  144. Query("field", field).
  145. FieldValue("cfg", h.JSONString(cfg)).
  146. Go()),
  147. ).
  148. Height(200).
  149. Class("d-flex align-center justify-center pa-6").
  150. Attr("role", "button").
  151. Attr("v-ripple", true),
  152. ),
  153. ).
  154. Cols(6).Sm(4).Md(3),
  155. VCol(
  156. VCard(
  157. VProgressCircular().
  158. Color("primary").
  159. Indeterminate(true),
  160. ).
  161. Class("d-flex align-center justify-center").
  162. Height(200),
  163. ).
  164. Attr("v-for", "f in locals.fileChooserUploadingFiles").
  165. Cols(6).Sm(4).Md(3),
  166. ),
  167. )
  168. var initCroppingVars = []string{fileCroppingVarName(0) + ": false"}
  169. for i, f := range files {
  170. _, needCrop := mergeNewSizes(f, cfg)
  171. croppingVar := fileCroppingVarName(f.ID)
  172. initCroppingVars = append(initCroppingVars, fmt.Sprintf("%s: false", croppingVar))
  173. imgClickVars := fmt.Sprintf("vars.mediaShow = '%s'; vars.mediaName = '%s'; vars.isImage = %s", f.File.URL(), f.File.FileName, strconv.FormatBool(media.IsImageFormat(f.File.FileName)))
  174. row.AppendChildren(
  175. VCol(
  176. VCard(
  177. h.Div(
  178. h.If(
  179. media.IsImageFormat(f.File.FileName),
  180. VImg(
  181. h.If(needCrop,
  182. h.Div(
  183. VProgressCircular().Indeterminate(true),
  184. h.Span(msgr.Cropping).Class("text-h6 pl-2"),
  185. ).Class("d-flex align-center justify-center v-card--reveal white--text").
  186. Style("height: 100%; background: rgba(0, 0, 0, 0.5)").
  187. Attr("v-if", fmt.Sprintf("locals.%s", croppingVar)),
  188. ),
  189. ).Src(f.File.URL(media_library.QorPreviewSizeName)).Height(200).Contain(true),
  190. ).Else(
  191. fileThumb(f.File.FileName),
  192. ),
  193. ).AttrIf("role", "button", field != mediaLibraryListField).
  194. AttrIf("@click", web.Plaid().
  195. BeforeScript(fmt.Sprintf("locals.%s = true", croppingVar)).
  196. EventFunc(chooseFileEvent).
  197. Query("field", field).
  198. Query("id", fmt.Sprint(f.ID)).
  199. FieldValue("cfg", h.JSONString(cfg)).
  200. Go(), field != mediaLibraryListField).
  201. AttrIf("@click", imgClickVars, field == mediaLibraryListField),
  202. VCardText(
  203. h.A().Text(f.File.FileName).
  204. Attr("@click", imgClickVars),
  205. h.Input("").
  206. Style("width: 100%;").
  207. Placeholder(msgr.DescriptionForAccessibility).
  208. Value(f.File.Description).
  209. Attr("@change", web.Plaid().
  210. EventFunc(updateDescriptionEvent).
  211. Query("field", field).
  212. Query("id", fmt.Sprint(f.ID)).
  213. FieldValue("cfg", h.JSONString(cfg)).
  214. FieldValue("CurrentDescription", web.Var("$event.target.value")).
  215. Go(),
  216. ).Readonly(updateDescIsAllowed(ctx.R, files[i]) != nil),
  217. h.If(media.IsImageFormat(f.File.FileName),
  218. fileChips(f),
  219. ),
  220. ),
  221. h.If(deleteIsAllowed(ctx.R, files[i]) == nil,
  222. VCardActions(
  223. VSpacer(),
  224. VBtn(msgr.Delete).
  225. Text(true).
  226. Attr("@click",
  227. web.Plaid().
  228. EventFunc(deleteConfirmationEvent).
  229. Query("field", field).
  230. Query("id", fmt.Sprint(f.ID)).
  231. FieldValue("cfg", h.JSONString(cfg)).
  232. Go(),
  233. ),
  234. ),
  235. ),
  236. ),
  237. ).Cols(6).Sm(4).Md(3),
  238. )
  239. }
  240. return h.Div(
  241. VSnackbar(h.Text(msgr.DescriptionUpdated)).
  242. Attr("v-model", "vars.snackbarShow").
  243. Top(true).
  244. Color("primary").
  245. Timeout(5000),
  246. web.Scope(
  247. VContainer(
  248. h.If(field == mediaLibraryListField,
  249. VRow(
  250. VCol(
  251. VSelect().Items([]selectItem{
  252. {Text: msgr.UploadedAtDESC, Value: orderByCreatedAtDESC},
  253. {Text: msgr.UploadedAt, Value: orderByCreatedAt},
  254. }).ItemText("Text").ItemValue("Value").
  255. Label(msgr.OrderBy).
  256. FieldName(orderByKey).Value(orderBy).
  257. Attr("@change",
  258. web.GET().PushState(true).
  259. Query(orderByKey, web.Var("[$event]")).Go(),
  260. ).
  261. Dense(true).Solo(true).Class("mb-n8"),
  262. ).Cols(3),
  263. ).Justify("end"),
  264. ),
  265. row,
  266. VRow(
  267. VCol().Cols(1),
  268. VCol(
  269. VPagination().
  270. Length(pagesCount).
  271. Value(int(currentPageInt)).
  272. Attr("@input", web.Plaid().
  273. FieldValue(currentPageName(field), web.Var("$event")).
  274. EventFunc(imageJumpPageEvent).
  275. Query("field", field).
  276. FieldValue("cfg", h.JSONString(cfg)).
  277. Go()),
  278. ).Cols(10),
  279. ),
  280. VCol().Cols(1),
  281. ).Fluid(true),
  282. ).Init(fmt.Sprintf(`{fileChooserUploadingFiles: [], %s}`, strings.Join(initCroppingVars, ", "))).VSlot("{ locals }"),
  283. VOverlay(
  284. h.Img("").Attr(":src", "vars.isImage? vars.mediaShow: ''").
  285. Style("max-height: 80vh; max-width: 80vw; background: rgba(0, 0, 0, 0.5)"),
  286. h.Div(
  287. h.A(
  288. VIcon("info").Small(true).Class("mb-1"),
  289. h.Text("{{vars.mediaName}}"),
  290. ).Attr(":href", "vars.mediaShow? vars.mediaShow: ''").Target("_blank").
  291. Class("white--text").Style("text-decoration: none;"),
  292. ).Class("d-flex align-center justify-center pt-2"),
  293. ).Attr("v-if", "vars.mediaName").Attr("@click", "vars.mediaName = null").ZIndex(10),
  294. ).Attr(web.InitContextVars, `{snackbarShow: false, mediaShow: null, mediaName: null, isImage: false}`)
  295. }
  296. func fileChips(f *media_library.MediaLibrary) h.HTMLComponent {
  297. g := VChipGroup().Column(true)
  298. text := "original"
  299. if f.File.Width != 0 && f.File.Height != 0 {
  300. text = fmt.Sprintf("%s(%dx%d)", "original", f.File.Width, f.File.Height)
  301. }
  302. if f.File.FileSizes["original"] != 0 {
  303. text = fmt.Sprintf("%s %s", text, media.ByteCountSI(f.File.FileSizes["original"]))
  304. }
  305. g.AppendChildren(
  306. VChip(h.Text(text)).XSmall(true),
  307. )
  308. // if len(f.File.Sizes) == 0 {
  309. // return g
  310. // }
  311. // for k, size := range f.File.GetSizes() {
  312. // g.AppendChildren(
  313. // VChip(thumbName(k, size)).XSmall(true),
  314. // )
  315. // }
  316. return g
  317. }
  318. type uploadFiles struct {
  319. NewFiles []*multipart.FileHeader
  320. }
  321. func uploadFile(db *gorm.DB) web.EventFunc {
  322. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  323. field := ctx.R.FormValue("field")
  324. cfg := stringToCfg(ctx.R.FormValue("cfg"))
  325. if err = uploadIsAllowed(ctx.R); err != nil {
  326. return
  327. }
  328. var uf uploadFiles
  329. ctx.MustUnmarshalForm(&uf)
  330. for _, fh := range uf.NewFiles {
  331. m := media_library.MediaLibrary{}
  332. if media.IsImageFormat(fh.Filename) {
  333. m.SelectedType = media_library.ALLOW_TYPE_IMAGE
  334. } else if media.IsVideoFormat(fh.Filename) {
  335. m.SelectedType = media_library.ALLOW_TYPE_VIDEO
  336. } else {
  337. m.SelectedType = media_library.ALLOW_TYPE_FILE
  338. }
  339. err = m.File.Scan(fh)
  340. if err != nil {
  341. panic(err)
  342. }
  343. err = media.SaveUploadAndCropImage(db, &m)
  344. if err != nil {
  345. presets.ShowMessage(&r, err.Error(), "error")
  346. return r, nil
  347. }
  348. }
  349. renderFileChooserDialogContent(ctx, &r, field, db, cfg)
  350. return
  351. }
  352. }
  353. func mergeNewSizes(m *media_library.MediaLibrary, cfg *media_library.MediaBoxConfig) (sizes map[string]*media.Size, r bool) {
  354. sizes = make(map[string]*media.Size)
  355. for k, size := range cfg.Sizes {
  356. if m.File.Sizes[k] != nil {
  357. sizes[k] = m.File.Sizes[k]
  358. continue
  359. }
  360. sizes[k] = size
  361. r = true
  362. }
  363. return
  364. }
  365. func chooseFile(db *gorm.DB) web.EventFunc {
  366. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  367. field := ctx.R.FormValue("field")
  368. id := ctx.QueryAsInt("id")
  369. cfg := stringToCfg(ctx.R.FormValue("cfg"))
  370. var m media_library.MediaLibrary
  371. err = db.Find(&m, id).Error
  372. if err != nil {
  373. return
  374. }
  375. sizes, needCrop := mergeNewSizes(&m, cfg)
  376. if needCrop {
  377. err = m.ScanMediaOptions(media_library.MediaOption{
  378. Sizes: sizes,
  379. Crop: true,
  380. })
  381. if err != nil {
  382. return
  383. }
  384. err = db.Save(&m).Error
  385. if err != nil {
  386. return
  387. }
  388. err = media.SaveUploadAndCropImage(db, &m)
  389. if err != nil {
  390. presets.ShowMessage(&r, err.Error(), "error")
  391. return r, nil
  392. }
  393. }
  394. mediaBox := media_library.MediaBox{
  395. ID: json.Number(fmt.Sprint(m.ID)),
  396. Url: m.File.Url,
  397. VideoLink: "",
  398. FileName: m.File.FileName,
  399. Description: m.File.Description,
  400. FileSizes: m.File.FileSizes,
  401. Width: m.File.Width,
  402. Height: m.File.Height,
  403. }
  404. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  405. Name: mediaBoxThumbnailsPortalName(field),
  406. Body: mediaBoxThumbnails(ctx, &mediaBox, field, cfg, false),
  407. })
  408. r.VarsScript = `vars.showFileChooser = false`
  409. return
  410. }
  411. }
  412. func searchFile(db *gorm.DB) web.EventFunc {
  413. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  414. field := ctx.R.FormValue("field")
  415. cfg := stringToCfg(ctx.R.FormValue("cfg"))
  416. ctx.R.Form[currentPageName(field)] = []string{"1"}
  417. renderFileChooserDialogContent(ctx, &r, field, db, cfg)
  418. return
  419. }
  420. }
  421. func jumpPage(db *gorm.DB) web.EventFunc {
  422. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  423. field := ctx.R.FormValue("field")
  424. cfg := stringToCfg(ctx.R.FormValue("cfg"))
  425. renderFileChooserDialogContent(ctx, &r, field, db, cfg)
  426. return
  427. }
  428. }
  429. func renderFileChooserDialogContent(ctx *web.EventContext, r *web.EventResponse, field string, db *gorm.DB, cfg *media_library.MediaBoxConfig) {
  430. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  431. Name: dialogContentPortalName(field),
  432. Body: fileChooserDialogContent(db, field, ctx, cfg),
  433. })
  434. }