Add feedback form
Signed-off-by: Michael Mayer <michael@lastzero.net>
This commit is contained in:
parent
4fc693fb0b
commit
b87e860444
|
@ -11,11 +11,11 @@
|
|||
</v-flex>
|
||||
|
||||
<v-flex xs12 sm6 class="px-0 pb-2 body-1 text-xs-left text-sm-right">
|
||||
<a href="https://docs.photoprism.org/credits/" class="secondary-dark--text"
|
||||
<a href="https://docs.photoprism.org/credits/" class="primary--text text--darken-4"
|
||||
target="_blank">Thank you</a> to everyone who made this possible!
|
||||
<br>
|
||||
<a href="https://raw.githubusercontent.com/photoprism/photoprism/develop/NOTICE"
|
||||
class="secondary-dark--text" target="_blank">
|
||||
class="primary--text text--darken-4" target="_blank">
|
||||
3rd-party software packages</a>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
|
|
|
@ -363,7 +363,15 @@
|
|||
<v-list-tile :to="{ name: 'about' }" :exact="true" @click="" class="nav-about">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate key="Help">Help</translate>
|
||||
<translate>About</translate>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile :to="{ name: 'feedback' }" :exact="true" @click="" v-show="!public && auth" class="nav-feedback">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Feedback</translate>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
|
|
@ -10,10 +10,13 @@
|
|||
|
||||
#photoprism p a.highlight,
|
||||
#photoprism #photoprism-help p a{
|
||||
color: #222;
|
||||
text-decoration: none;
|
||||
text-shadow: -3px 0px 3px rgba(84, 226, 255, 0.15), 3px 0px 3px rgba(84, 226, 255, 0.15), 6px 0px 6px rgba(84, 226, 255, 0.15), -6px 0px 6px rgba(84, 226, 255, 0.15);
|
||||
/* text-shadow: -3px 0px 3px rgba(255, 255, 0, 0.5), 3px 0px 3px rgba(255, 255, 0, 0.5), 6px 0px 6px rgba(255, 255, 0, 0.5), -6px 0px 6px rgba(255, 255, 0, 0.5); */
|
||||
color: #4a148c;
|
||||
}
|
||||
|
||||
#photoprism p a.highlight:hover,
|
||||
#photoprism #photoprism-help p a:hover{
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
#photoprism .v-alert a {
|
||||
|
|
|
@ -257,7 +257,7 @@
|
|||
const months = Info.months("long");
|
||||
|
||||
for (let i = 0; i < months.length; i++) {
|
||||
result.push({"Month": i + 1, "Name": months[i]});
|
||||
result.push({"Month": i + 1, "UserName": months[i]});
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -236,3 +236,12 @@ export const Colors = () => [
|
|||
{"Example": "#9E9E9E", "Name": $gettext("Grey"), "Slug": "grey"},
|
||||
{"Example": "#212121", "Name": $gettext("Black"), "Slug": "black"},
|
||||
];
|
||||
|
||||
export const FeedbackCategories = () => [
|
||||
{"value": "help", "text": $gettext("Customer Support")},
|
||||
{"value": "feedback", "text": $gettext("Product Feedback")},
|
||||
{"value": "feature", "text": $gettext("Feature Request")},
|
||||
{"value": "bug", "text": $gettext("Bug Report")},
|
||||
{"value": "donations", "text": $gettext("Donations")},
|
||||
{"value": "other", "text": $gettext("Other")},
|
||||
];
|
|
@ -2,7 +2,7 @@
|
|||
<div class="p-page p-page-about">
|
||||
<v-toolbar flat color="secondary">
|
||||
<v-toolbar-title>
|
||||
<translate>Whatever it is, we'd love to hear from you!</translate>
|
||||
<translate>Contributors</translate>
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
@ -12,12 +12,71 @@
|
|||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-container fluid class="pa-4">
|
||||
<p class="body-1">Feel free to use the
|
||||
<a target="_blank" href="https://gitter.im/browseyourlife/community" class="highlight">chat</a>
|
||||
or send an e-mail to <a target="_blank" href="mailto:hello@photoprism.org" class="highlight">hello@photoprism.org</a>
|
||||
if you have questions, want to support our work, or just want to say hello.</p>
|
||||
<h3 class="py-2 body-2">Andrea Ceroni</h3>
|
||||
<p class="body-1">Andrea is a data scientist specialized in temporal information retrieval and machine
|
||||
learning.
|
||||
He holds a PhD in Computer Science from the Leibniz University of Hannover (L3S Research Center) and wrote
|
||||
numerous papers on topics such as
|
||||
<a target="_blank"
|
||||
href="https://www.researchgate.net/profile/Andrea_Ceroni/publication/323222448_Personal_Photo_Management_and_Preservation/links/5a995f8da6fdcc3cbac8fa59/Personal-Photo-Management-and-Preservation.pdf"
|
||||
class="primary--text text--darken-4">Personal
|
||||
Photo Management and Preservation</a>
|
||||
and <a target="_blank" href="https://www.iti.gr/~bmezaris/publications/hmmp@icme2015_2_preprint.pdf"
|
||||
class="primary--text text--darken-4">Photo Selection Models for
|
||||
Personal Photo Collections</a>.</p>
|
||||
<p class="body-1">You can find him on <a target="_blank"
|
||||
href="https://scholar.google.de/citations?user=JHsQY5YAAAAJ&hl=en"
|
||||
class="primary--text text--darken-4">Google Scholar</a> and <a
|
||||
target="_blank"
|
||||
href="https://www.linkedin.com/in/andrea-ceroni/" class="primary--text text--darken-4">LinkedIn</a></p>
|
||||
|
||||
<p>
|
||||
<h3 class="py-2 body-2">Theresa Gresch</h3>
|
||||
<p class="body-1">Theresa works as a freelance product manager and developer in Berlin. She has a Master's in
|
||||
Neurobiology
|
||||
and aims to dive deeper into machine learning while working on this project.</p>
|
||||
<p class="body-1">You can find her on <a target="_blank" href="https://github.com/graciousgrey"
|
||||
class="primary--text text--darken-4">GitHub</a> and <a
|
||||
target="_blank" href="https://www.linkedin.com/in/theresa-gresch-886924103/"
|
||||
class="primary--text text--darken-4">LinkedIn</a></p>
|
||||
|
||||
<h3 class="py-2 body-2">Michael Mayer</h3>
|
||||
<p class="body-1">Michael learned coding on an Atari 1040 ST and started his first open-source projects in
|
||||
the 90s. He has more than 25 years of experience in building Web applications.
|
||||
His motivation is to explore the latest technologies and build an amazing product outside the constraints of a
|
||||
corporate environment.</p>
|
||||
<p class="body-1">You can find him on <a target="_blank" href="https://github.com/lastzero"
|
||||
class="primary--text text--darken-4">GitHub</a>,
|
||||
<a target="_blank" href="https://www.linkedin.com/in/lastzero/"
|
||||
class="primary--text text--darken-4">LinkedIn</a>
|
||||
and <a target="_blank" href="https://twitter.com/lastzero" class="primary--text text--darken-4">Twitter</a>
|
||||
</p>
|
||||
|
||||
<h3 class="py-2 body-2">Guy Sheffer</h3>
|
||||
<p class="body-1">Known as <a target="_blank" href="https://github.com/guysoft">GuySoft</a> on the web. Active
|
||||
developer in
|
||||
the Free Software and Maker community.
|
||||
Creator of <a target="_blank" href="https://github.com/guysoft/OctoPi" class="primary--text text--darken-4">OctoPi</a>
|
||||
and
|
||||
<a target="_blank" href="https://github.com/guysoft/FullPageOS"
|
||||
class="primary--text text--darken-4">FullPageOS</a>,
|
||||
which have hundreds of thousands of downloads. Raspberry Pi distro expert.
|
||||
Currently VP R&D and Co-Founder at <a target="_blank" href="https://shapedo.com/"
|
||||
class="primary--text text--darken-4">ShapeDo</a>.</p>
|
||||
|
||||
<p class="body-2 pb-1">
|
||||
<a target="_blank" href="https://github.com/photoprism/photoprism/graphs/contributors"
|
||||
class="primary--text text--darken-4">...and many more</a>
|
||||
</p>
|
||||
|
||||
<h2 class="py-2 subheading">
|
||||
<translate>Trademarks</translate>
|
||||
</h2>
|
||||
<p class="body-1 pb-2">
|
||||
<translate>PhotoPrism® is a registered trademark of Michael Mayer.</translate>
|
||||
<translate>You may use it as required to describe our software, run your own server, for educational purposes, but not for offering commercial goods, products, or services without prior written permission. In other words, please ask.</translate>
|
||||
</p>
|
||||
|
||||
<p class="text-xs-center pt-3 mb-2">
|
||||
<router-link to="/about/license">
|
||||
<img src="/static/img/badge-agpl.svg" alt="License AGPL v3" style="max-width:100%;"/>
|
||||
</router-link>
|
||||
|
@ -29,59 +88,6 @@
|
|||
<a target="_blank" href="https://twitter.com/browseyourlife" rel="nofollow"><img
|
||||
src="/static/img/badge-twitter.svg" alt="Twitter" style="max-width:100%;"></a>
|
||||
</p>
|
||||
|
||||
<h2 class="py-2 subheading"><translate>Who we are</translate></h2>
|
||||
|
||||
<h3 class="py-2 body-2">Andrea Ceroni</h3>
|
||||
<p class="body-1">Andrea is a data scientist specialized in temporal information retrieval and machine
|
||||
learning.
|
||||
He holds a PhD in Computer Science from the Leibniz University of Hannover (L3S Research Center) and wrote
|
||||
numerous papers on topics such as
|
||||
<a target="_blank" href="https://www.researchgate.net/profile/Andrea_Ceroni/publication/323222448_Personal_Photo_Management_and_Preservation/links/5a995f8da6fdcc3cbac8fa59/Personal-Photo-Management-and-Preservation.pdf" class="highlight">Personal
|
||||
Photo Management and Preservation</a>
|
||||
and <a target="_blank" href="https://www.iti.gr/~bmezaris/publications/hmmp@icme2015_2_preprint.pdf" class="highlight">Photo Selection Models for
|
||||
Personal Photo Collections</a>.</p>
|
||||
<p class="body-1">You can find him on <a target="_blank"
|
||||
href="https://scholar.google.de/citations?user=JHsQY5YAAAAJ&hl=en" class="highlight">Google Scholar</a> and <a target="_blank"
|
||||
href="https://www.linkedin.com/in/andrea-ceroni/" class="highlight">LinkedIn</a></p>
|
||||
|
||||
<h3 class="py-2 body-2">Theresa Gresch</h3>
|
||||
<p class="body-1">Theresa works as a freelance product manager and developer in Berlin. She has a Master's in
|
||||
Neurobiology
|
||||
and aims to dive deeper into machine learning while working on this project.</p>
|
||||
<p class="body-1">You can find her on <a target="_blank" href="https://github.com/graciousgrey" class="highlight">GitHub</a> and <a
|
||||
target="_blank" href="https://www.linkedin.com/in/theresa-gresch-886924103/" class="highlight">LinkedIn</a></p>
|
||||
|
||||
<h3 class="py-2 body-2">Michael Mayer</h3>
|
||||
<p class="body-1">Michael learned coding on an Atari 1040 ST and started his first open-source projects in
|
||||
the 90s. He has more than 20 years of experience in building Web applications.
|
||||
His motivation is to explore the latest technologies and build an amazing product outside the constraints of a
|
||||
corporate environment.</p>
|
||||
<p class="body-1">You can find him on <a target="_blank" href="https://github.com/lastzero" class="highlight">GitHub</a>,
|
||||
<a target="_blank" href="https://www.linkedin.com/in/lastzero/" class="highlight">LinkedIn</a>
|
||||
and <a target="_blank" href="https://twitter.com/lastzero" class="highlight">Twitter</a>
|
||||
</p>
|
||||
|
||||
<h3 class="py-2 body-2">Guy Sheffer</h3>
|
||||
<p class="body-1">Known as <a target="_blank" href="https://github.com/guysoft">GuySoft</a> on the web. Active
|
||||
developer in
|
||||
the Free Software and Maker community.
|
||||
Creator of <a target="_blank" href="https://github.com/guysoft/OctoPi" class="highlight">OctoPi</a> and
|
||||
<a target="_blank" href="https://github.com/guysoft/FullPageOS" class="highlight">FullPageOS</a>,
|
||||
which have hundreds of thousands of downloads. Raspberry Pi distro expert.
|
||||
Currently VP R&D and Co-Founder at <a target="_blank" href="https://shapedo.com/" class="highlight">ShapeDo</a>.</p>
|
||||
|
||||
<h3 class="pb-1 body-2">...and many more</h3>
|
||||
<p class="body-1">Thank you to
|
||||
<a target="_blank" href="https://github.com/photoprism/photoprism/graphs/contributors" class="highlight">everyone who
|
||||
contributed</a> to this project!</p>
|
||||
|
||||
|
||||
<h2 class="py-2 subheading"><translate>Trademarks</translate></h2>
|
||||
<p class="body-1">
|
||||
<translate>PhotoPrism® is a registered trademark of Michael Mayer.</translate>
|
||||
<translate>You may use it as required to describe our software, run your own server, for educational purposes, but not for offering commercial goods, products, or services without prior written permission. In other words, please ask.</translate>
|
||||
</p>
|
||||
</v-container>
|
||||
|
||||
<p-about-footer></p-about-footer>
|
||||
|
@ -89,11 +95,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
export default {
|
||||
name: 'p-page-about',
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
|
121
frontend/src/pages/about/feedback.vue
Normal file
121
frontend/src/pages/about/feedback.vue
Normal file
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="p-page p-page-support">
|
||||
<v-toolbar flat color="secondary">
|
||||
<v-toolbar-title v-if="sent">
|
||||
<translate>Your message has been sent</translate>
|
||||
</v-toolbar-title>
|
||||
<v-toolbar-title v-else>
|
||||
<translate>Whatever it is, we'd love to hear from you!</translate>
|
||||
</v-toolbar-title>
|
||||
|
||||
<!-- v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon href="https://github.com/photoprism/photoprism" target="_blank" class="action-github" title="GitHub">
|
||||
<img src="/static/brands/github.svg" width="24" alt="GitHub">
|
||||
</v-btn -->
|
||||
</v-toolbar>
|
||||
<v-container fluid class="pa-4" v-if="sent">
|
||||
<p class="body-1">We'll get back to you as soon as possible!</p>
|
||||
</v-container>
|
||||
<v-form autocomplete="off" class="pa-3" ref="form"
|
||||
v-model="valid"
|
||||
lazy-validation v-else>
|
||||
<v-layout row wrap>
|
||||
<!-- v-flex xs12 sm6 lg4 xl2 grow class="pa-2">
|
||||
<v-text-field :required="true" hide-details
|
||||
v-model="form.Subject" browser-autocomplete="off"
|
||||
:label="$gettext('Subject')"></v-text-field>
|
||||
</v-flex -->
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-select
|
||||
:disabled="busy"
|
||||
:items="options.FeedbackCategories()"
|
||||
:label="$gettext('Category')"
|
||||
color="secondary-dark"
|
||||
background-color="secondary-light"
|
||||
v-model="form.Category"
|
||||
flat solo hide-details required
|
||||
browser-autocomplete="off"
|
||||
class="input-category"
|
||||
:rules="[v => !!v || $gettext('Required')]"
|
||||
></v-select>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 class="pa-2">
|
||||
<v-textarea required auto-grow flat solo hide-details browser-autocomplete="off"
|
||||
v-model="form.Message" rows="10" :rules="[v => !!v || $gettext('Required')]"
|
||||
:label="$gettext('Your Message')"></v-textarea>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field flat solo hide-details required browser-autocomplete="off"
|
||||
color="secondary-dark" :rules="[v => !!v || $gettext('Required')]"
|
||||
background-color="secondary-light"
|
||||
:label="$gettext('E-Mail')" type="email" v-model="form.UserEmail">
|
||||
</v-text-field>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 sm6 class="pa-2">
|
||||
<v-text-field flat solo hide-details browser-autocomplete="off"
|
||||
color="secondary-dark"
|
||||
background-color="secondary-light"
|
||||
:label="$gettext('Name')" type="text" v-model="form.UserName">
|
||||
</v-text-field>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 grow class="px-2 py-1">
|
||||
<v-btn color="secondary-dark"
|
||||
class="white--text ml-0"
|
||||
depressed
|
||||
:disabled="!form.Category || !form.Message || !form.UserEmail"
|
||||
@click.stop="send">
|
||||
<translate>Send</translate>
|
||||
<v-icon right dark>send</v-icon>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
|
||||
</v-layout>
|
||||
|
||||
</v-form>
|
||||
|
||||
<p-about-footer></p-about-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as options from "options/options";
|
||||
import Api from "../../common/api";
|
||||
|
||||
export default {
|
||||
name: 'p-page-support',
|
||||
data() {
|
||||
return {
|
||||
sent: false,
|
||||
busy: false,
|
||||
valid: false,
|
||||
options: options,
|
||||
form: {
|
||||
Category: "",
|
||||
Message: "",
|
||||
UserName: "",
|
||||
UserEmail: "",
|
||||
UserAgent: navigator.userAgent,
|
||||
UserLocales: navigator.language,
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
send() {
|
||||
if (this.$refs.form.validate()) {
|
||||
Api.post("feedback", this.form).then(() => {
|
||||
this.$notify.success(this.$gettext("Message sent"));
|
||||
this.sent = true;
|
||||
});
|
||||
} else {
|
||||
this.$notify.error(this.$gettext("All fields are required"));
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -41,6 +41,7 @@ import Settings from "pages/settings.vue";
|
|||
import Login from "pages/login.vue";
|
||||
import Discover from "pages/discover.vue";
|
||||
import About from "pages/about/about.vue";
|
||||
import Feedback from "pages/about/feedback.vue";
|
||||
import License from "pages/about/license.vue";
|
||||
import Help from "pages/help.vue";
|
||||
import {$gettext} from "common/vm";
|
||||
|
@ -59,6 +60,12 @@ export default [
|
|||
component: About,
|
||||
meta: {title: c.name, auth: false},
|
||||
},
|
||||
{
|
||||
name: "feedback",
|
||||
path: "/feedback",
|
||||
component: Feedback,
|
||||
meta: {title: c.name, auth: false},
|
||||
},
|
||||
{
|
||||
name: "license",
|
||||
path: "/about/license",
|
||||
|
|
|
@ -22,4 +22,5 @@ const (
|
|||
ResourcePeople Resource = "people"
|
||||
ResourcePhotos Resource = "photos"
|
||||
ResourcePlaces Resource = "places"
|
||||
ResourceFeedback Resource = "feedback"
|
||||
)
|
||||
|
|
47
internal/api/feedback.go
Normal file
47
internal/api/feedback.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// POST /api/v1/feedback
|
||||
func SendFeedback(router *gin.RouterGroup) {
|
||||
router.POST("/feedback", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceFeedback, acl.ActionCreate)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
conf.UpdatePro()
|
||||
|
||||
var f form.Feedback
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
if f.Empty() {
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
|
||||
return
|
||||
}
|
||||
|
||||
if err := conf.Pro().SendFeedback(f); err != nil {
|
||||
log.Error(err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK})
|
||||
})
|
||||
}
|
|
@ -26,9 +26,6 @@ type ClientConfig struct {
|
|||
ReadOnly bool `json:"readonly"`
|
||||
UploadNSFW bool `json:"uploadNSFW"`
|
||||
Public bool `json:"public"`
|
||||
Pro bool `json:"pro"`
|
||||
Sponsor bool `json:"sponsor"`
|
||||
Contributor bool `json:"contributor"`
|
||||
Experimental bool `json:"experimental"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
AlbumCategories []string `json:"albumCategories"`
|
||||
|
@ -37,7 +34,7 @@ type ClientConfig struct {
|
|||
Lenses []entity.Lens `json:"lenses"`
|
||||
Countries []entity.Country `json:"countries"`
|
||||
Thumbs []Thumb `json:"thumbs"`
|
||||
ApiKey string `json:"apiKey"`
|
||||
Status string `json:"status"`
|
||||
MapKey string `json:"mapKey"`
|
||||
DownloadToken string `json:"downloadToken"`
|
||||
PreviewToken string `json:"previewToken"`
|
||||
|
@ -144,7 +141,7 @@ func (c *Config) PublicConfig() ClientConfig {
|
|||
DisableSettings: c.SettingsHidden(),
|
||||
Public: c.Public(),
|
||||
Experimental: c.Experimental(),
|
||||
ApiKey: "",
|
||||
Status: "",
|
||||
MapKey: "",
|
||||
Thumbs: Thumbs,
|
||||
Colors: colors.All.List(),
|
||||
|
@ -187,7 +184,7 @@ func (c *Config) GuestConfig() ClientConfig {
|
|||
Experimental: false,
|
||||
Colors: colors.All.List(),
|
||||
Thumbs: Thumbs,
|
||||
ApiKey: c.Pro().ApiKey(),
|
||||
Status: c.Pro().Status,
|
||||
MapKey: c.Pro().MapKey(),
|
||||
DownloadToken: c.DownloadToken(),
|
||||
PreviewToken: c.PreviewToken(),
|
||||
|
@ -221,7 +218,7 @@ func (c *Config) UserConfig() ClientConfig {
|
|||
Experimental: c.Experimental(),
|
||||
Colors: colors.All.List(),
|
||||
Thumbs: Thumbs,
|
||||
ApiKey: c.Pro().ApiKey(),
|
||||
Status: c.Pro().Status,
|
||||
MapKey: c.Pro().MapKey(),
|
||||
DownloadToken: c.DownloadToken(),
|
||||
PreviewToken: c.PreviewToken(),
|
||||
|
|
|
@ -269,6 +269,19 @@ func (c *Config) OriginalsLimit() int64 {
|
|||
return c.params.OriginalsLimit * 1024 * 1024
|
||||
}
|
||||
|
||||
// UpdatePro updates photoprism.pro api credentials.
|
||||
func (c *Config) UpdatePro() {
|
||||
p := c.ProConfigFile()
|
||||
|
||||
if err := c.pro.Refresh(); err != nil {
|
||||
log.Errorf("pro: %s", err)
|
||||
} else if err := c.pro.Save(p); err != nil {
|
||||
log.Errorf("pro: %s", err)
|
||||
} else {
|
||||
c.pro.Propagate()
|
||||
}
|
||||
}
|
||||
|
||||
// initPro initializes photoprism.pro api credentials.
|
||||
func (c *Config) initPro() {
|
||||
c.pro = pro.NewConfig(c.Version())
|
||||
|
@ -290,13 +303,7 @@ func (c *Config) initPro() {
|
|||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := c.pro.Refresh(); err != nil {
|
||||
log.Errorf("pro: %s", err)
|
||||
} else if err := c.pro.Save(p); err != nil {
|
||||
log.Errorf("pro: %s", err)
|
||||
} else {
|
||||
c.pro.Propagate()
|
||||
}
|
||||
c.UpdatePro()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
|
15
internal/form/feedback.go
Normal file
15
internal/form/feedback.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package form
|
||||
|
||||
// Feedback represents support requests / customer feedback.
|
||||
type Feedback struct {
|
||||
Category string `json:"Category"`
|
||||
Message string `json:"Message"`
|
||||
UserName string `json:"UserName"`
|
||||
UserEmail string `json:"UserEmail"`
|
||||
UserAgent string `json:"UserAgent"`
|
||||
UserLocales string `json:"UserLocales"`
|
||||
}
|
||||
|
||||
func (f Feedback) Empty() bool {
|
||||
return len(f.Category) < 1 || len(f.Message) < 3 || len(f.UserEmail) < 5
|
||||
}
|
|
@ -41,11 +41,6 @@ func NewConfig(version string) *Config {
|
|||
}
|
||||
}
|
||||
|
||||
// ApiKey returns the photoprism.pro api key.
|
||||
func (c *Config) ApiKey() string {
|
||||
return c.Key
|
||||
}
|
||||
|
||||
// MapKey returns the maps api key.
|
||||
func (c *Config) MapKey() string {
|
||||
if sess, err := c.DecodeSession(); err != nil {
|
||||
|
|
89
internal/pro/feedback.go
Normal file
89
internal/pro/feedback.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package pro
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
var FeedbackURL = ApiURL + "/%s/feedback"
|
||||
|
||||
type Feedback struct {
|
||||
Key string `json:"ApiKey"`
|
||||
ClientVersion string `json:"ClientVersion"`
|
||||
ClientOS string `json:"ClientOS"`
|
||||
ClientArch string `json:"ClientArch"`
|
||||
ClientCPU int `json:"ClientCPU"`
|
||||
Category string `json:"Category"`
|
||||
Message string `json:"Message"`
|
||||
UserName string `json:"UserName"`
|
||||
UserEmail string `json:"UserEmail"`
|
||||
UserAgent string `json:"UserAgent"`
|
||||
}
|
||||
|
||||
// NewFeedback creates a new photoprism.pro key request instance.
|
||||
func NewFeedback(version string) *Feedback {
|
||||
return &Feedback{
|
||||
ClientVersion: version,
|
||||
ClientOS: runtime.GOOS,
|
||||
ClientArch: runtime.GOARCH,
|
||||
ClientCPU: runtime.NumCPU(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) SendFeedback(f form.Feedback) (err error) {
|
||||
feedback := NewFeedback(c.Version)
|
||||
feedback.Category = f.Category
|
||||
feedback.Message = f.Message
|
||||
feedback.UserName = f.UserName
|
||||
feedback.UserEmail = f.UserEmail
|
||||
feedback.UserAgent = f.UserAgent
|
||||
feedback.Key = c.Key
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
url := fmt.Sprintf(FeedbackURL, c.Key)
|
||||
method := http.MethodPost
|
||||
var req *http.Request
|
||||
|
||||
log.Debugf("pro: sending feedback")
|
||||
|
||||
if j, err := json.Marshal(feedback); err != nil {
|
||||
return err
|
||||
} else if req, err = http.NewRequest(method, url, bytes.NewReader(j)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("Accept-Language", f.UserLocales)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
var r *http.Response
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
r, err = client.Do(req)
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("pro: %s", err.Error())
|
||||
return err
|
||||
} else if r.StatusCode >= 400 {
|
||||
err = fmt.Errorf("sending feedback failed with code %d", r.StatusCode)
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(c)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("pro: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -111,6 +111,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.SaveSettings(v1)
|
||||
api.ChangePassword(v1)
|
||||
api.GetErrors(v1)
|
||||
api.SendFeedback(v1)
|
||||
|
||||
api.GetSvg(v1)
|
||||
|
||||
|
|
Loading…
Reference in a new issue