media_box.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. package views
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "path"
  7. "sort"
  8. "time"
  9. "github.com/qor5/admin/media"
  10. "github.com/qor5/admin/media/media_library"
  11. "github.com/qor5/admin/presets"
  12. "github.com/qor5/ui/cropper"
  13. "github.com/qor5/ui/fileicons"
  14. . "github.com/qor5/ui/vuetify"
  15. "github.com/qor5/web"
  16. "github.com/qor5/x/i18n"
  17. "github.com/qor5/x/perm"
  18. "github.com/sunfmin/reflectutils"
  19. h "github.com/theplant/htmlgo"
  20. "golang.org/x/text/language"
  21. "gorm.io/gorm"
  22. )
  23. type MediaBoxConfigKey int
  24. var MediaLibraryPerPage int = 39
  25. const MediaBoxConfig MediaBoxConfigKey = iota
  26. const I18nMediaLibraryKey i18n.ModuleKey = "I18nMediaLibraryKey"
  27. var permVerifier *perm.Verifier
  28. func Configure(b *presets.Builder, db *gorm.DB) {
  29. err := db.AutoMigrate(&media_library.MediaLibrary{})
  30. if err != nil {
  31. panic(err)
  32. }
  33. b.ExtraAsset("/cropper.js", "text/javascript", cropper.JSComponentsPack())
  34. b.ExtraAsset("/cropper.css", "text/css", cropper.CSSComponentsPack())
  35. permVerifier = perm.NewVerifier("media_library", b.GetPermission())
  36. b.FieldDefaults(presets.WRITE).
  37. FieldType(media_library.MediaBox{}).
  38. ComponentFunc(MediaBoxComponentFunc(db)).
  39. SetterFunc(MediaBoxSetterFunc(db))
  40. b.FieldDefaults(presets.LIST).
  41. FieldType(media_library.MediaBox{}).
  42. ComponentFunc(MediaBoxListFunc())
  43. registerEventFuncs(b.GetWebBuilder(), db)
  44. b.I18n().
  45. RegisterForModule(language.English, I18nMediaLibraryKey, Messages_en_US).
  46. RegisterForModule(language.SimplifiedChinese, I18nMediaLibraryKey, Messages_zh_CN).
  47. RegisterForModule(language.Japanese, I18nMediaLibraryKey, Messages_ja_JP)
  48. configList(b, db)
  49. }
  50. func MediaBoxComponentFunc(db *gorm.DB) presets.FieldComponentFunc {
  51. return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  52. cfg, ok := field.ContextValue(MediaBoxConfig).(*media_library.MediaBoxConfig)
  53. if !ok {
  54. cfg = &media_library.MediaBoxConfig{}
  55. }
  56. mediaBox := field.Value(obj).(media_library.MediaBox)
  57. return QMediaBox(db).
  58. FieldName(field.FormKey).
  59. Value(&mediaBox).
  60. Label(field.Label).
  61. Config(cfg).Disabled(field.Disabled)
  62. }
  63. }
  64. func MediaBoxSetterFunc(db *gorm.DB) presets.FieldSetterFunc {
  65. return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
  66. jsonValuesField := fmt.Sprintf("%s.Values", field.FormKey)
  67. mediaBox := media_library.MediaBox{}
  68. err = mediaBox.Scan(ctx.R.FormValue(jsonValuesField))
  69. if err != nil {
  70. return
  71. }
  72. descriptionField := fmt.Sprintf("%s.Description", field.FormKey)
  73. mediaBox.Description = ctx.R.FormValue(descriptionField)
  74. err = reflectutils.Set(obj, field.Name, mediaBox)
  75. if err != nil {
  76. return
  77. }
  78. return
  79. }
  80. }
  81. type QMediaBoxBuilder struct {
  82. fieldName string
  83. label string
  84. value *media_library.MediaBox
  85. config *media_library.MediaBoxConfig
  86. db *gorm.DB
  87. disabled bool
  88. }
  89. func QMediaBox(db *gorm.DB) (r *QMediaBoxBuilder) {
  90. r = &QMediaBoxBuilder{
  91. db: db,
  92. }
  93. return
  94. }
  95. func (b *QMediaBoxBuilder) FieldName(v string) (r *QMediaBoxBuilder) {
  96. b.fieldName = v
  97. return b
  98. }
  99. func (b *QMediaBoxBuilder) Value(v *media_library.MediaBox) (r *QMediaBoxBuilder) {
  100. b.value = v
  101. return b
  102. }
  103. func (b *QMediaBoxBuilder) Disabled(v bool) (r *QMediaBoxBuilder) {
  104. b.disabled = v
  105. return b
  106. }
  107. func (b *QMediaBoxBuilder) Label(v string) (r *QMediaBoxBuilder) {
  108. b.label = v
  109. return b
  110. }
  111. func (b *QMediaBoxBuilder) Config(v *media_library.MediaBoxConfig) (r *QMediaBoxBuilder) {
  112. b.config = v
  113. return b
  114. }
  115. func (b *QMediaBoxBuilder) MarshalHTML(c context.Context) (r []byte, err error) {
  116. if len(b.fieldName) == 0 {
  117. panic("FieldName required")
  118. }
  119. if b.value == nil {
  120. panic("Value required")
  121. }
  122. ctx := web.MustGetEventContext(c)
  123. portalName := mainPortalName(b.fieldName)
  124. return h.Components(
  125. VSheet(
  126. h.If(len(b.label) > 0,
  127. h.Label(b.label).Class("v-label theme--light"),
  128. ),
  129. web.Portal(
  130. mediaBoxThumbnails(ctx, b.value, b.fieldName, b.config, b.disabled),
  131. ).Name(mediaBoxThumbnailsPortalName(b.fieldName)),
  132. web.Portal().Name(portalName),
  133. ).Class("pb-4").
  134. Rounded(true).
  135. Attr(web.InitContextVars, `{showFileChooser: false}`),
  136. ).MarshalHTML(c)
  137. }
  138. func mediaBoxThumb(msgr *Messages, cfg *media_library.MediaBoxConfig,
  139. f *media_library.MediaBox, field string, thumb string, disabled bool) h.HTMLComponent {
  140. size := cfg.Sizes[thumb]
  141. fileSize := f.FileSizes[thumb]
  142. url := f.URL(thumb)
  143. if thumb == media.DefaultSizeKey {
  144. url = f.URL()
  145. }
  146. return VCard(
  147. h.If(media.IsImageFormat(f.FileName),
  148. VImg().Src(fmt.Sprintf("%s?%d", url, time.Now().UnixNano())).Height(150),
  149. ).Else(
  150. h.Div(
  151. fileThumb(f.FileName),
  152. h.A().Text(f.FileName).Href(f.Url).Target("_blank"),
  153. ).Style("text-align:center"),
  154. ),
  155. h.If(media.IsImageFormat(f.FileName) && (size != nil || thumb == media.DefaultSizeKey),
  156. VCardActions(
  157. VChip(
  158. thumbName(thumb, size, fileSize, f),
  159. ).Small(true).Disabled(disabled).Attr("@click", web.Plaid().
  160. EventFunc(loadImageCropperEvent).
  161. Query("field", field).
  162. Query("id", fmt.Sprint(f.ID)).
  163. Query("thumb", thumb).
  164. FieldValue("cfg", h.JSONString(cfg)).
  165. Go()),
  166. ),
  167. ),
  168. )
  169. }
  170. func fileThumb(filename string) h.HTMLComponent {
  171. return h.Div(
  172. fileicons.Icon(path.Ext(filename)[1:]).Attr("height", "150").Class("pt-4"),
  173. ).Class("d-flex align-center justify-center")
  174. }
  175. func deleteConfirmation(db *gorm.DB) web.EventFunc {
  176. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  177. msgr := i18n.MustGetModuleMessages(ctx.R, presets.CoreI18nModuleKey, Messages_en_US).(*presets.Messages)
  178. field := ctx.R.FormValue("field")
  179. id := ctx.R.FormValue("id")
  180. cfg := ctx.R.FormValue("cfg")
  181. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  182. Name: deleteConfirmPortalName(field),
  183. Body: VDialog(
  184. VCard(
  185. VCardTitle(h.Text(msgr.DeleteConfirmationText(id))),
  186. VCardActions(
  187. VSpacer(),
  188. VBtn(msgr.Cancel).
  189. Depressed(true).
  190. Class("ml-2").
  191. On("click", "vars.mediaLibrary_deleteConfirmation = false"),
  192. VBtn(msgr.Delete).
  193. Color("primary").
  194. Depressed(true).
  195. Dark(true).
  196. Attr("@click", web.Plaid().
  197. EventFunc(doDeleteEvent).
  198. Query("field", field).
  199. Query("id", id).
  200. FieldValue("cfg", cfg).
  201. Go()),
  202. ),
  203. ),
  204. ).MaxWidth("600px").
  205. Attr("v-model", "vars.mediaLibrary_deleteConfirmation").
  206. Attr(web.InitContextVars, `{mediaLibrary_deleteConfirmation: false}`),
  207. })
  208. r.VarsScript = "setTimeout(function(){ vars.mediaLibrary_deleteConfirmation = true }, 100)"
  209. return
  210. }
  211. }
  212. func doDelete(db *gorm.DB) web.EventFunc {
  213. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  214. field := ctx.R.FormValue("field")
  215. id := ctx.R.FormValue("id")
  216. cfg := ctx.R.FormValue("cfg")
  217. var obj media_library.MediaLibrary
  218. err = db.Where("id = ?", id).First(&obj).Error
  219. if err != nil {
  220. if err == gorm.ErrRecordNotFound {
  221. renderFileChooserDialogContent(
  222. ctx,
  223. &r,
  224. field,
  225. db,
  226. stringToCfg(cfg),
  227. )
  228. r.VarsScript = "vars.mediaLibrary_deleteConfirmation = false"
  229. return r, nil
  230. }
  231. panic(err)
  232. }
  233. if err = deleteIsAllowed(ctx.R, &obj); err != nil {
  234. return
  235. }
  236. err = db.Delete(&media_library.MediaLibrary{}, "id = ?", id).Error
  237. if err != nil {
  238. panic(err)
  239. }
  240. renderFileChooserDialogContent(
  241. ctx,
  242. &r,
  243. field,
  244. db,
  245. stringToCfg(cfg),
  246. )
  247. r.VarsScript = "vars.mediaLibrary_deleteConfirmation = false"
  248. return
  249. }
  250. }
  251. func mediaBoxThumbnails(ctx *web.EventContext, mediaBox *media_library.MediaBox, field string, cfg *media_library.MediaBoxConfig, disabled bool) h.HTMLComponent {
  252. msgr := i18n.MustGetModuleMessages(ctx.R, I18nMediaLibraryKey, Messages_en_US).(*Messages)
  253. c := VContainer().Fluid(true)
  254. if mediaBox.ID.String() != "" && mediaBox.ID.String() != "0" {
  255. row := VRow()
  256. if len(cfg.Sizes) == 0 {
  257. row.AppendChildren(
  258. VCol(
  259. mediaBoxThumb(msgr, cfg, mediaBox, field, media.DefaultSizeKey, disabled),
  260. ).Cols(6).Sm(4).Class("pl-0"),
  261. )
  262. } else {
  263. var keys []string
  264. for k, _ := range cfg.Sizes {
  265. keys = append(keys, k)
  266. }
  267. sort.Strings(keys)
  268. for _, k := range keys {
  269. row.AppendChildren(
  270. VCol(
  271. mediaBoxThumb(msgr, cfg, mediaBox, field, k, disabled),
  272. ).Cols(6).Sm(4).Class("pl-0"),
  273. )
  274. }
  275. }
  276. c.AppendChildren(row)
  277. if media.IsImageFormat(mediaBox.FileName) {
  278. fieldName := fmt.Sprintf("%s.Description", field)
  279. value := ctx.R.FormValue(fieldName)
  280. if len(value) == 0 {
  281. value = mediaBox.Description
  282. }
  283. c.AppendChildren(
  284. VRow(
  285. VCol(
  286. VTextField().
  287. Value(value).
  288. Attr(web.VFieldName(fieldName)...).
  289. Label(msgr.DescriptionForAccessibility).
  290. Dense(true).
  291. HideDetails(true).
  292. Outlined(true).
  293. Disabled(disabled),
  294. ).Cols(12).Class("pl-0 pt-0"),
  295. ),
  296. )
  297. }
  298. }
  299. mediaBoxValue := ""
  300. if mediaBox.ID.String() != "" && mediaBox.ID.String() != "0" {
  301. mediaBoxValue = h.JSONString(mediaBox)
  302. }
  303. return h.Components(
  304. c,
  305. web.Portal().Name(cropperPortalName(field)),
  306. h.Input("").Type("hidden").
  307. Value(mediaBoxValue).
  308. Attr(web.VFieldName(fmt.Sprintf("%s.Values", field))...),
  309. VBtn(msgr.ChooseFile).
  310. Depressed(true).
  311. Attr("@click", web.Plaid().EventFunc(openFileChooserEvent).
  312. Query("field", field).
  313. FieldValue("cfg", h.JSONString(cfg)).
  314. Go(),
  315. ).Disabled(disabled),
  316. h.If(mediaBox != nil && mediaBox.ID.String() != "" && mediaBox.ID.String() != "0",
  317. VBtn(msgr.Delete).
  318. Depressed(true).
  319. Attr("@click", web.Plaid().EventFunc(deleteFileEvent).
  320. Query("field", field).
  321. FieldValue("cfg", h.JSONString(cfg)).
  322. Go(),
  323. ).Disabled(disabled),
  324. ),
  325. )
  326. }
  327. func MediaBoxListFunc() presets.FieldComponentFunc {
  328. return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
  329. mediaBox := field.Value(obj).(media_library.MediaBox)
  330. return h.Td(h.Img("").Src(mediaBox.URL(media_library.QorPreviewSizeName)).Style("height: 48px;"))
  331. }
  332. }
  333. func deleteFileField() web.EventFunc {
  334. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  335. field := ctx.R.FormValue("field")
  336. cfg := stringToCfg(ctx.R.FormValue("cfg"))
  337. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  338. Name: mediaBoxThumbnailsPortalName(field),
  339. Body: mediaBoxThumbnails(ctx, &media_library.MediaBox{}, field, cfg, false),
  340. })
  341. return
  342. }
  343. }
  344. func stringToCfg(v string) *media_library.MediaBoxConfig {
  345. var cfg media_library.MediaBoxConfig
  346. if len(v) == 0 {
  347. return &cfg
  348. }
  349. err := json.Unmarshal([]byte(v), &cfg)
  350. if err != nil {
  351. panic(err)
  352. }
  353. return &cfg
  354. }
  355. func thumbName(name string, size *media.Size, fileSize int, f *media_library.MediaBox) h.HTMLComponent {
  356. text := name
  357. if size != nil {
  358. text = fmt.Sprintf("%s(%dx%d)", text, size.Width, size.Height)
  359. }
  360. if name == media.DefaultSizeKey {
  361. text = fmt.Sprintf("%s(%dx%d)", text, f.Width, f.Height)
  362. }
  363. if fileSize != 0 {
  364. text = fmt.Sprintf("%s %s", text, media.ByteCountSI(fileSize))
  365. }
  366. return h.Text(text)
  367. }
  368. func updateDescription(db *gorm.DB) web.EventFunc {
  369. return func(ctx *web.EventContext) (r web.EventResponse, err error) {
  370. field := ctx.R.FormValue("field")
  371. id := ctx.R.FormValue("id")
  372. cfg := ctx.R.FormValue("cfg")
  373. var obj media_library.MediaLibrary
  374. err = db.Where("id = ?", id).First(&obj).Error
  375. if err != nil {
  376. if err == gorm.ErrRecordNotFound {
  377. renderFileChooserDialogContent(
  378. ctx,
  379. &r,
  380. field,
  381. db,
  382. stringToCfg(cfg),
  383. )
  384. // TODO: prompt that the record has been deleted?
  385. return r, nil
  386. }
  387. panic(err)
  388. }
  389. if err = updateDescIsAllowed(ctx.R, &obj); err != nil {
  390. return
  391. }
  392. var media media_library.MediaLibrary
  393. if err = db.Find(&media, id).Error; err != nil {
  394. return
  395. }
  396. media.File.Description = ctx.R.FormValue("CurrentDescription")
  397. if err = db.Save(&media).Error; err != nil {
  398. return
  399. }
  400. r.VarsScript = `vars.snackbarShow = true;`
  401. return
  402. }
  403. }