Merge branch 'develop'
This commit is contained in:
commit
824fb9d063
18
.fossa.yml
Executable file
18
.fossa.yml
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
|
||||||
|
# Visit https://fossa.com to learn more
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
cli:
|
||||||
|
server: https://app.fossa.com
|
||||||
|
fetcher: custom
|
||||||
|
project: git@github.com:photoprism/photoprism.git
|
||||||
|
analyze:
|
||||||
|
modules:
|
||||||
|
- name: github.com/photoprism/photoprism/cmd/photoprism
|
||||||
|
type: go
|
||||||
|
target: github.com/photoprism/photoprism/cmd/photoprism
|
||||||
|
path: cmd/photoprism
|
||||||
|
- name: frontend
|
||||||
|
type: npm
|
||||||
|
target: frontend
|
||||||
|
path: frontend
|
|
@ -1,4 +1,4 @@
|
||||||
FROM photoprism/development:20190919
|
FROM photoprism/development:20191105
|
||||||
|
|
||||||
# Set up project directory
|
# Set up project directory
|
||||||
WORKDIR "/go/src/github.com/photoprism/photoprism"
|
WORKDIR "/go/src/github.com/photoprism/photoprism"
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
debug: false
|
debug: false
|
||||||
|
read-only: false
|
||||||
|
public: false
|
||||||
|
admin-password: photoprism
|
||||||
config-path: ~/.config/photoprism
|
config-path: ~/.config/photoprism
|
||||||
cache-path: ~/.cache/photoprism
|
cache-path: ~/.cache/photoprism
|
||||||
assets-path: ~/.local/share/photoprism
|
assets-path: ~/.local/share/photoprism
|
||||||
|
@ -12,7 +15,6 @@ sql-password: photoprism
|
||||||
http-host:
|
http-host:
|
||||||
http-mode: release
|
http-mode: release
|
||||||
http-port: 2342
|
http-port: 2342
|
||||||
http-password:
|
|
||||||
database-driver: internal
|
database-driver: internal
|
||||||
database-dsn: root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true
|
database-dsn: root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true
|
||||||
pid-filename: ~/.local/share/photoprism/photoprism.pid
|
pid-filename: ~/.local/share/photoprism/photoprism.pid
|
||||||
|
|
2
assets/config/settings.yml
Normal file
2
assets/config/settings.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
theme: dark
|
||||||
|
language: en
|
|
@ -33,14 +33,16 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
name: "{{ .name }}",
|
"name": "{{ .name }}",
|
||||||
version: "{{ .version }}",
|
"version": "{{ .version }}",
|
||||||
copyright: "{{ .copyright }}",
|
"copyright": "{{ .copyright }}",
|
||||||
debug: {{ .debug }},
|
"debug": {{ .debug }},
|
||||||
readonly: {{ .readonly }},
|
"readonly": {{ .readonly }},
|
||||||
cameras: {{ .cameras }},
|
"public": {{ .public }},
|
||||||
countries: {{ .countries }},
|
"cameras": {{ .cameras }},
|
||||||
thumbnails: {{ .thumbnails }}
|
"countries": {{ .countries }},
|
||||||
|
"thumbnails": {{ .thumbnails }},
|
||||||
|
"settings": {{ .settings }},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -63,6 +65,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="p-busy-overlay"></div>
|
||||||
|
|
||||||
<script src="/static/build/app.js?{{ .jsHash }}"></script>
|
<script src="/static/build/app.js?{{ .jsHash }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,7 +5,7 @@ services:
|
||||||
build: .
|
build: .
|
||||||
image: photoprism/photoprism:develop
|
image: photoprism/photoprism:develop
|
||||||
depends_on:
|
depends_on:
|
||||||
- photoprism-mysql
|
- photoprism-db
|
||||||
command: tail -f /dev/null
|
command: tail -f /dev/null
|
||||||
volumes:
|
volumes:
|
||||||
- "~/.cache/npm:/root/.cache/npm"
|
- "~/.cache/npm:/root/.cache/npm"
|
||||||
|
@ -34,8 +34,8 @@ services:
|
||||||
CI_BUILD_ID:
|
CI_BUILD_ID:
|
||||||
CI_JOB_ID:
|
CI_JOB_ID:
|
||||||
|
|
||||||
photoprism-mysql:
|
photoprism-db:
|
||||||
image: mysql:8.0.16
|
image: mariadb:10.4.8
|
||||||
command: mysqld --port=4001 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=1024
|
command: mysqld --port=4001 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=1024
|
||||||
expose:
|
expose:
|
||||||
- "4001"
|
- "4001"
|
||||||
|
|
|
@ -5,7 +5,7 @@ services:
|
||||||
build: .
|
build: .
|
||||||
image: photoprism/photoprism:develop
|
image: photoprism/photoprism:develop
|
||||||
depends_on:
|
depends_on:
|
||||||
- photoprism-mysql
|
- photoprism-db
|
||||||
ports:
|
ports:
|
||||||
- "2342:2342" # Web Server (PhotoPrism)
|
- "2342:2342" # Web Server (PhotoPrism)
|
||||||
- "4000:4000" # Database (MySQL compatible)
|
- "4000:4000" # Database (MySQL compatible)
|
||||||
|
@ -31,8 +31,8 @@ services:
|
||||||
PHOTOPRISM_SQL_PASSWORD: "photoprism"
|
PHOTOPRISM_SQL_PASSWORD: "photoprism"
|
||||||
TF_CPP_MIN_LOG_LEVEL: 0
|
TF_CPP_MIN_LOG_LEVEL: 0
|
||||||
|
|
||||||
photoprism-mysql:
|
photoprism-db:
|
||||||
image: mysql:8.0.16
|
image: mariadb:10.4.8
|
||||||
command: mysqld --port=4001 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=1024
|
command: mysqld --port=4001 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=1024
|
||||||
expose:
|
expose:
|
||||||
- "4001"
|
- "4001"
|
||||||
|
|
|
@ -9,4 +9,4 @@ RUN wget -qO- https://dl.photoprism.org/fixtures/demo.tar.gz | tar xvz -C Pictur
|
||||||
RUN photoprism import
|
RUN photoprism import
|
||||||
|
|
||||||
# Start PhotoPrism server
|
# Start PhotoPrism server
|
||||||
CMD photoprism start
|
CMD photoprism --public start
|
||||||
|
|
|
@ -82,12 +82,12 @@ RUN npm install --unsafe-perm=true --allow-root -g npm testcafe chromedriver
|
||||||
RUN npm config set cache ~/.cache/npm
|
RUN npm config set cache ~/.cache/npm
|
||||||
|
|
||||||
# Install Go
|
# Install Go
|
||||||
ENV GOLANG_VERSION 1.13
|
ENV GOLANG_VERSION 1.13.4
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
\
|
\
|
||||||
url="https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz"; \
|
url="https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz"; \
|
||||||
wget -O go.tgz "$url"; \
|
wget -O go.tgz "$url"; \
|
||||||
echo "68a2297eb099d1a76097905a2ce334e3155004ec08cdea85f24527be3c48e856 *go.tgz" | sha256sum -c -; \
|
echo "692d17071736f74be04a72a06dab9cac1cd759377bd85316e52b2227604c004c *go.tgz" | sha256sum -c -; \
|
||||||
tar -C /usr/local -xzf go.tgz; \
|
tar -C /usr/local -xzf go.tgz; \
|
||||||
rm go.tgz; \
|
rm go.tgz; \
|
||||||
export PATH="/usr/local/go/bin:$PATH"; \
|
export PATH="/usr/local/go/bin:$PATH"; \
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM photoprism/development:20190919 as build
|
FROM photoprism/development:20191105 as build
|
||||||
|
|
||||||
# Set up project directory
|
# Set up project directory
|
||||||
WORKDIR "/go/src/github.com/photoprism/photoprism"
|
WORKDIR "/go/src/github.com/photoprism/photoprism"
|
||||||
|
|
2572
frontend/package-lock.json
generated
2572
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -14,34 +14,34 @@
|
||||||
"test-firefox": "testcafe firefox:headless --selector-timeout 5000 -S -s tests/screenshots tests/acceptance"
|
"test-firefox": "testcafe firefox:headless --selector-timeout 5000 -S -s tests/screenshots tests/acceptance"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/cli": "^7.6.0",
|
"@babel/cli": "^7.7.0",
|
||||||
"@babel/core": "^7.6.0",
|
"@babel/core": "^7.7.2",
|
||||||
"@babel/plugin-transform-runtime": "^7.6.0",
|
"@babel/plugin-transform-runtime": "^7.6.2",
|
||||||
"@babel/polyfill": "^7.6.0",
|
"@babel/polyfill": "^7.7.0",
|
||||||
"@babel/preset-env": "^7.6.0",
|
"@babel/preset-env": "^7.7.1",
|
||||||
"@babel/register": "^7.6.0",
|
"@babel/register": "^7.7.0",
|
||||||
"@fortawesome/fontawesome-free": "^5.10.2",
|
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||||
"@types/leaflet": "^1.5.1",
|
"@types/leaflet": "^1.5.5",
|
||||||
"acorn": "^6.3.0",
|
"acorn": "^6.3.0",
|
||||||
"ajv": "^6.10.2",
|
"ajv": "^6.10.2",
|
||||||
"autoprefixer": "^9.6.1",
|
"autoprefixer": "^9.7.1",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"axios-mock-adapter": "^1.17.0",
|
"axios-mock-adapter": "^1.17.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-istanbul": "^5.2.0",
|
"babel-plugin-istanbul": "^5.2.0",
|
||||||
"browserslist": "^4.7.0",
|
"browserslist": "^4.7.2",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chalk": "^2.4.2",
|
"chalk": "^2.4.2",
|
||||||
"chart.js": "^2.5.0",
|
"chart.js": "^2.9.2",
|
||||||
"chrome-finder": "^1.0.5",
|
"chrome-finder": "^1.0.6",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"connect-history-api-fallback": "^1.3.0",
|
"connect-history-api-fallback": "^1.3.0",
|
||||||
"copy-webpack-plugin": "^5.0.4",
|
"copy-webpack-plugin": "^5.0.5",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"css-loader": "^2.1.1",
|
"css-loader": "^2.1.1",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"eslint": "^6.4.0",
|
"eslint": "^6.6.0",
|
||||||
"eslint-config-standard": "^13.0.1",
|
"eslint-config-standard": "^13.0.1",
|
||||||
"eslint-formatter-pretty": "^2.1.1",
|
"eslint-formatter-pretty": "^2.1.1",
|
||||||
"eslint-friendly-formatter": "^4.0.1",
|
"eslint-friendly-formatter": "^4.0.1",
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"http-proxy-middleware": "^0.19.1",
|
"http-proxy-middleware": "^0.19.1",
|
||||||
"inject-loader": "^4.0.1",
|
"inject-loader": "^4.0.1",
|
||||||
"karma": "^4.3.0",
|
"karma": "^4.4.1",
|
||||||
"karma-chrome-launcher": "^3.1.0",
|
"karma-chrome-launcher": "^3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "^2.1.0",
|
"karma-coverage-istanbul-reporter": "^2.1.0",
|
||||||
"karma-htmlfile-reporter": "^0.3.8",
|
"karma-htmlfile-reporter": "^0.3.8",
|
||||||
|
@ -65,16 +65,16 @@
|
||||||
"karma-verbose-reporter": "^0.0.6",
|
"karma-verbose-reporter": "^0.0.6",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-webpack": "^4.0.2",
|
||||||
"leaflet": "^1.5.1",
|
"leaflet": "^1.5.1",
|
||||||
"luxon": "^1.17.3",
|
"luxon": "^1.21.1",
|
||||||
"material-design-icons-iconfont": "^5.0.1",
|
"material-design-icons-iconfont": "^5.0.1",
|
||||||
"mini-css-extract-plugin": "^0.7.0",
|
"mini-css-extract-plugin": "^0.7.0",
|
||||||
"mocha": "^6.2.0",
|
"mocha": "^6.2.2",
|
||||||
"moment-timezone": "^0.5.26",
|
"moment-timezone": "^0.5.27",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
"ora": "^3.4.0",
|
"ora": "^3.4.0",
|
||||||
"photoswipe": "^4.1.3",
|
"photoswipe": "^4.1.3",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"postcss": "^7.0.18",
|
"postcss": "^7.0.21",
|
||||||
"postcss-browser-reporter": "^0.6.0",
|
"postcss-browser-reporter": "^0.6.0",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^12.0.1",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
|
@ -83,17 +83,19 @@
|
||||||
"postcss-url": "^8.0.0",
|
"postcss-url": "^8.0.0",
|
||||||
"pubsub-js": "^1.7.0",
|
"pubsub-js": "^1.7.0",
|
||||||
"puppeteer-core": "^1.20.0",
|
"puppeteer-core": "^1.20.0",
|
||||||
"resolve-url-loader": "^3.1.0",
|
"resolve-url-loader": "^3.1.1",
|
||||||
"sass-loader": "^7.3.1",
|
"sass-loader": "^7.3.1",
|
||||||
"sinon": "^7.4.2",
|
"sinon": "^7.5.0",
|
||||||
|
"sockette": "^2.0.6",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"sugarss": "^2.0.0",
|
"sugarss": "^2.0.0",
|
||||||
"svg-url-loader": "^2.3.3",
|
"svg-url-loader": "^2.3.3",
|
||||||
"tar": "^4.4.10",
|
"tar": "^4.4.13",
|
||||||
"truncate": "^2.1.0",
|
"truncate": "^2.1.0",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "^1.1.2",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-fullscreen": "^2.1.5",
|
"vue-fullscreen": "^2.1.5",
|
||||||
|
"vue-gettext": "^2.1.6",
|
||||||
"vue-infinite-scroll": "^2.0.2",
|
"vue-infinite-scroll": "^2.0.2",
|
||||||
"vue-loader": "^14.2.4",
|
"vue-loader": "^14.2.4",
|
||||||
"vue-luxon": "^0.7.0",
|
"vue-luxon": "^0.7.0",
|
||||||
|
@ -103,10 +105,10 @@
|
||||||
"vue2-filters": "^0.6.1",
|
"vue2-filters": "^0.6.1",
|
||||||
"vue2-leaflet": "^2.2.1",
|
"vue2-leaflet": "^2.2.1",
|
||||||
"vuelidate": "^0.7.4",
|
"vuelidate": "^0.7.4",
|
||||||
"vuetify": "^1.5.18",
|
"vuetify": "^1.5.21",
|
||||||
"webpack": "^4.40.2",
|
"webpack": "^4.41.2",
|
||||||
"webpack-bundle-analyzer": "^3.5.0",
|
"webpack-bundle-analyzer": "^3.6.0",
|
||||||
"webpack-cli": "^3.3.8",
|
"webpack-cli": "^3.3.10",
|
||||||
"webpack-hot-middleware": "^2.25.0",
|
"webpack-hot-middleware": "^2.25.0",
|
||||||
"webpack-md5-hash": "0.0.6",
|
"webpack-md5-hash": "0.0.6",
|
||||||
"webpack-merge": "^4.2.2"
|
"webpack-merge": "^4.2.2"
|
||||||
|
|
|
@ -1,36 +1,40 @@
|
||||||
import Vue from "vue";
|
|
||||||
import Vuetify from "vuetify";
|
|
||||||
import Router from "vue-router";
|
|
||||||
import PhotoPrism from "photoprism.vue";
|
|
||||||
import Routes from "routes";
|
|
||||||
import Api from "common/api";
|
import Api from "common/api";
|
||||||
|
import Notify from "common/notify";
|
||||||
import Config from "common/config";
|
import Config from "common/config";
|
||||||
import Clipboard from "common/clipboard";
|
import Clipboard from "common/clipboard";
|
||||||
import Components from "component/components";
|
import Components from "component/components";
|
||||||
import Dialogs from "dialog/dialogs";
|
import Dialogs from "dialog/dialogs";
|
||||||
import Maps from "maps/components";
|
|
||||||
import Alert from "common/alert";
|
|
||||||
import Viewer from "common/viewer";
|
|
||||||
import Session from "common/session";
|
|
||||||
import Event from "pubsub-js";
|
import Event from "pubsub-js";
|
||||||
import VueLuxon from "vue-luxon";
|
import GetTextPlugin from "vue-gettext";
|
||||||
import VueInfiniteScroll from "vue-infinite-scroll";
|
import Maps from "maps/components";
|
||||||
import VueFullscreen from "vue-fullscreen";
|
import PhotoPrism from "photoprism.vue";
|
||||||
import VueFilters from "vue2-filters";
|
import Router from "vue-router";
|
||||||
|
import Routes from "routes";
|
||||||
|
import Session from "session";
|
||||||
import { Settings } from "luxon";
|
import { Settings } from "luxon";
|
||||||
|
import Socket from "common/websocket";
|
||||||
|
import Translations from "./i18n/translations.json";
|
||||||
|
import Viewer from "common/viewer";
|
||||||
|
import Vue from "vue";
|
||||||
|
import Vuetify from "vuetify";
|
||||||
|
import VueLuxon from "vue-luxon";
|
||||||
|
import VueFilters from "vue2-filters";
|
||||||
|
import VueFullscreen from "vue-fullscreen";
|
||||||
|
import VueInfiniteScroll from "vue-infinite-scroll";
|
||||||
|
|
||||||
// Initialize helpers
|
// Initialize helpers
|
||||||
const session = new Session(window.localStorage);
|
|
||||||
const config = new Config(window.localStorage, window.appConfig);
|
const config = new Config(window.localStorage, window.appConfig);
|
||||||
const viewer = new Viewer();
|
const viewer = new Viewer();
|
||||||
const clipboard = new Clipboard(window.localStorage, "photo_clipboard");
|
const clipboard = new Clipboard(window.localStorage, "photo_clipboard");
|
||||||
|
const isPublic = config.getValue("public");
|
||||||
|
|
||||||
// Assign helpers to VueJS prototype
|
// Assign helpers to VueJS prototype
|
||||||
Vue.prototype.$event = Event;
|
Vue.prototype.$event = Event;
|
||||||
Vue.prototype.$alert = Alert;
|
Vue.prototype.$notify = Notify;
|
||||||
Vue.prototype.$viewer = viewer;
|
Vue.prototype.$viewer = viewer;
|
||||||
Vue.prototype.$session = session;
|
Vue.prototype.$session = Session;
|
||||||
Vue.prototype.$api = Api;
|
Vue.prototype.$api = Api;
|
||||||
|
Vue.prototype.$socket = Socket;
|
||||||
Vue.prototype.$config = config;
|
Vue.prototype.$config = config;
|
||||||
Vue.prototype.$clipboard = clipboard;
|
Vue.prototype.$clipboard = clipboard;
|
||||||
|
|
||||||
|
@ -49,9 +53,11 @@ Vue.use(Vuetify, {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Settings.defaultLocale = "en";
|
Vue.config.language = "en";
|
||||||
|
Settings.defaultLocale = Vue.config.language;
|
||||||
|
|
||||||
// Register other VueJS plugins
|
// Register other VueJS plugins
|
||||||
|
Vue.use(GetTextPlugin, {translations: Translations, silent: false, defaultLanguage: Vue.config.language});
|
||||||
Vue.use(VueLuxon);
|
Vue.use(VueLuxon);
|
||||||
Vue.use(VueInfiniteScroll);
|
Vue.use(VueInfiniteScroll);
|
||||||
Vue.use(VueFullscreen);
|
Vue.use(VueFullscreen);
|
||||||
|
@ -68,6 +74,30 @@ const router = new Router({
|
||||||
saveScrollPosition: true,
|
saveScrollPosition: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
if(to.matched.some(record => record.meta.admin)) {
|
||||||
|
if (isPublic || Session.isAdmin()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next({
|
||||||
|
name: "login",
|
||||||
|
params: { nextUrl: to.fullPath },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if(to.matched.some(record => record.meta.auth)) {
|
||||||
|
if (isPublic || Session.isUser()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next({
|
||||||
|
name: "login",
|
||||||
|
params: { nextUrl: to.fullPath },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Run app
|
// Run app
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import Event from "pubsub-js";
|
|
||||||
|
|
||||||
const Alert = {
|
|
||||||
info: function (message) {
|
|
||||||
Event.publish("alert.info", message);
|
|
||||||
},
|
|
||||||
warning: function (message) {
|
|
||||||
Event.publish("alert.warning", message);
|
|
||||||
},
|
|
||||||
error: function (message) {
|
|
||||||
Event.publish("alert.error", message);
|
|
||||||
},
|
|
||||||
success: function (message) {
|
|
||||||
Event.publish("alert.success", message);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Alert;
|
|
|
@ -1,8 +1,8 @@
|
||||||
import axios from "axios";
|
|
||||||
import Event from "pubsub-js";
|
|
||||||
import "@babel/polyfill/noConflict";
|
import "@babel/polyfill/noConflict";
|
||||||
|
import Axios from "axios";
|
||||||
|
import Notify from "common/notify";
|
||||||
|
|
||||||
const Api = axios.create({
|
const Api = Axios.create({
|
||||||
baseURL: "/api/v1",
|
baseURL: "/api/v1",
|
||||||
headers: {common: {
|
headers: {common: {
|
||||||
"X-Session-Token": window.localStorage.getItem("session_token"),
|
"X-Session-Token": window.localStorage.getItem("session_token"),
|
||||||
|
@ -11,7 +11,7 @@ const Api = axios.create({
|
||||||
|
|
||||||
Api.interceptors.request.use(function (config) {
|
Api.interceptors.request.use(function (config) {
|
||||||
// Do something before request is sent
|
// Do something before request is sent
|
||||||
Event.publish("ajax.start", config);
|
Notify.ajaxStart();
|
||||||
return config;
|
return config;
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
// Do something with request error
|
// Do something with request error
|
||||||
|
@ -19,10 +19,15 @@ Api.interceptors.request.use(function (config) {
|
||||||
});
|
});
|
||||||
|
|
||||||
Api.interceptors.response.use(function (response) {
|
Api.interceptors.response.use(function (response) {
|
||||||
Event.publish("ajax.end", response);
|
Notify.ajaxEnd();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
|
Notify.ajaxEnd();
|
||||||
|
|
||||||
|
if (Axios.isCancel(error)) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
if(console && console.log) {
|
if(console && console.log) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
@ -36,12 +41,7 @@ Api.interceptors.response.use(function (response) {
|
||||||
errorMessage = data.message ? data.message : data.error;
|
errorMessage = data.message ? data.message : data.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
Event.publish("ajax.end");
|
Notify.error(errorMessage);
|
||||||
Event.publish("alert.error", errorMessage);
|
|
||||||
|
|
||||||
if(code === 401) {
|
|
||||||
window.location = "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Api from "common/api";
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
class Config {
|
class Config {
|
||||||
/**
|
/**
|
||||||
|
@ -11,15 +11,14 @@ class Config {
|
||||||
|
|
||||||
this.values = values;
|
this.values = values;
|
||||||
|
|
||||||
// this.setValues(JSON.parse(this.storage.getItem(this.storage_key)));
|
this.subscriptionId = Event.subscribe('config.updated', (ev, data) => this.setValues(data));
|
||||||
// this.setValues(values);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setValues(values) {
|
setValues(values) {
|
||||||
if(!values) return;
|
if (!values) return;
|
||||||
|
|
||||||
for(let key in values) {
|
for (let key in values) {
|
||||||
if(values.hasOwnProperty(key)) {
|
if (values.hasOwnProperty(key)) {
|
||||||
this.setValue(key, values[key]);
|
this.setValue(key, values[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,14 +51,6 @@ class Config {
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
pullFromServer() {
|
|
||||||
return Api.get("config").then(
|
|
||||||
(result) => {
|
|
||||||
this.setValues(result.data);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Config;
|
export default Config;
|
||||||
|
|
38
frontend/src/common/notify.js
Normal file
38
frontend/src/common/notify.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
|
const Notify = {
|
||||||
|
info: function (message) {
|
||||||
|
Event.publish("notify.info", {msg: message});
|
||||||
|
},
|
||||||
|
warning: function (message) {
|
||||||
|
Event.publish("notify.warning", {msg: message});
|
||||||
|
},
|
||||||
|
error: function (message) {
|
||||||
|
Event.publish("notify.error", {msg: message});
|
||||||
|
},
|
||||||
|
success: function (message) {
|
||||||
|
Event.publish("notify.success", {msg: message});
|
||||||
|
},
|
||||||
|
ajaxStart: function() {
|
||||||
|
Event.publish("ajax.start");
|
||||||
|
},
|
||||||
|
ajaxEnd: function() {
|
||||||
|
Event.publish("ajax.end");
|
||||||
|
},
|
||||||
|
blockUI: function() {
|
||||||
|
const el = document.getElementById('p-busy-overlay');
|
||||||
|
|
||||||
|
if(el) {
|
||||||
|
el.style.display = 'block';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unblockUI: function() {
|
||||||
|
const el = document.getElementById('p-busy-overlay');
|
||||||
|
|
||||||
|
if(el) {
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Notify;
|
|
@ -1,23 +1,55 @@
|
||||||
import Api from "common/api";
|
import Api from "./api";
|
||||||
import User from "model/user";
|
import User from "../model/user";
|
||||||
|
|
||||||
class Session {
|
export default class Session {
|
||||||
/**
|
/**
|
||||||
* @param {Storage} storage
|
* @param {Storage} storage
|
||||||
*/
|
*/
|
||||||
constructor(storage) {
|
constructor(storage) {
|
||||||
this.storage = storage;
|
this.auth = false;
|
||||||
this.session_token = this.storage.getItem("session_token");
|
|
||||||
|
|
||||||
const userJson = this.storage.getItem("user");
|
if (storage.getItem("session_storage") === "true") {
|
||||||
|
this.storage = window.sessionStorage;
|
||||||
|
} else {
|
||||||
|
this.storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
this.user = userJson !== "undefined" ? new User(JSON.parse(userJson)) : null;
|
if (this.applyToken(this.storage.getItem("session_token"))) {
|
||||||
|
const userJson = this.storage.getItem("user");
|
||||||
|
this.user = userJson !== "undefined" ? new User(JSON.parse(userJson)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isUser()) {
|
||||||
|
this.auth = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useSessionStorage() {
|
||||||
|
this.deleteToken();
|
||||||
|
this.storage.setItem("session_storage", "true");
|
||||||
|
this.storage = window.sessionStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
useLocalStorage() {
|
||||||
|
this.storage.setItem("session_storage", "false");
|
||||||
|
this.storage = window.localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyToken(token) {
|
||||||
|
if (!token) {
|
||||||
|
this.deleteToken();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.session_token = token;
|
||||||
|
Api.defaults.headers.common["X-Session-Token"] = token;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setToken(token) {
|
setToken(token) {
|
||||||
this.session_token = token;
|
|
||||||
this.storage.setItem("session_token", token);
|
this.storage.setItem("session_token", token);
|
||||||
Api.defaults.headers.common["X-Session-Token"] = token;
|
return this.applyToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken() {
|
getToken() {
|
||||||
|
@ -27,13 +59,14 @@ class Session {
|
||||||
deleteToken() {
|
deleteToken() {
|
||||||
this.session_token = null;
|
this.session_token = null;
|
||||||
this.storage.removeItem("session_token");
|
this.storage.removeItem("session_token");
|
||||||
Api.defaults.headers.common["X-Session-Token"] = "";
|
delete Api.defaults.headers.common["X-Session-Token"];
|
||||||
this.deleteUser();
|
this.deleteUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(user) {
|
setUser(user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.storage.setItem("user", JSON.stringify(user.getValues()));
|
this.storage.setItem("user", JSON.stringify(user.getValues()));
|
||||||
|
this.auth = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser() {
|
getUser() {
|
||||||
|
@ -42,15 +75,7 @@ class Session {
|
||||||
|
|
||||||
getEmail() {
|
getEmail() {
|
||||||
if (this.isUser()) {
|
if (this.isUser()) {
|
||||||
return this.user.userEmail;
|
return this.user.Email;
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
getFullName() {
|
|
||||||
if (this.isUser()) {
|
|
||||||
return this.user.userFirstName + " " + this.user.userLastName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
@ -58,25 +83,34 @@ class Session {
|
||||||
|
|
||||||
getFirstName() {
|
getFirstName() {
|
||||||
if (this.isUser()) {
|
if (this.isUser()) {
|
||||||
return this.user.userFirstName;
|
return this.user.FirstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullName() {
|
||||||
|
if (this.isUser()) {
|
||||||
|
return this.user.FirstName + " " + this.user.LastName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
isUser() {
|
isUser() {
|
||||||
return this.user.hasId();
|
return this.user && this.user.hasId();
|
||||||
}
|
}
|
||||||
|
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
return this.user.hasId() && this.user.userRole === "admin";
|
return this.user && this.user.hasId() && this.user.Role === "admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
isAnonymous() {
|
isAnonymous() {
|
||||||
return !this.user.hasId();
|
return !this.user || !this.user.hasId();
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteUser() {
|
deleteUser() {
|
||||||
|
this.auth = false;
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.storage.removeItem("user");
|
this.storage.removeItem("user");
|
||||||
}
|
}
|
||||||
|
@ -84,7 +118,7 @@ class Session {
|
||||||
login(email, password) {
|
login(email, password) {
|
||||||
this.deleteToken();
|
this.deleteToken();
|
||||||
|
|
||||||
return Api.post("session", { email: email, password: password }).then(
|
return Api.post("session", {email: email, password: password}).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
this.setToken(result.data.token);
|
this.setToken(result.data.token);
|
||||||
this.setUser(new User(result.data.user));
|
this.setUser(new User(result.data.user));
|
||||||
|
@ -104,5 +138,3 @@ class Session {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Session;
|
|
||||||
|
|
22
frontend/src/common/websocket.js
Normal file
22
frontend/src/common/websocket.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import Sockette from "sockette";
|
||||||
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
|
const host = window.location.host;
|
||||||
|
const Socket = new Sockette("ws://" + host + "/api/v1/ws", {
|
||||||
|
timeout: 5e3,
|
||||||
|
onopen: e => {
|
||||||
|
console.log('Connected!', e);
|
||||||
|
Socket.send("hello world");
|
||||||
|
},
|
||||||
|
onmessage: e => {
|
||||||
|
const m = JSON.parse(e.data);
|
||||||
|
console.log('Received:', m);
|
||||||
|
Event.publish(m.event, m.data);
|
||||||
|
},
|
||||||
|
onreconnect: e => console.log('Reconnecting...', e),
|
||||||
|
onmaximum: e => console.log('Stop Attempting!', e),
|
||||||
|
onclose: e => console.log('Closed!', e),
|
||||||
|
onerror: e => console.log('Error:', e)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Socket;
|
|
@ -1,4 +1,4 @@
|
||||||
import PAlert from "./p-alert.vue";
|
import PNotify from "./p-notify.vue";
|
||||||
import PNavigation from "./p-navigation.vue";
|
import PNavigation from "./p-navigation.vue";
|
||||||
import PLoadingBar from "./p-loading-bar.vue";
|
import PLoadingBar from "./p-loading-bar.vue";
|
||||||
import PPhotoDetails from "./p-photo-details.vue";
|
import PPhotoDetails from "./p-photo-details.vue";
|
||||||
|
@ -13,7 +13,7 @@ import PScrollTop from "./p-scroll-top.vue";
|
||||||
const components = {};
|
const components = {};
|
||||||
|
|
||||||
components.install = (Vue) => {
|
components.install = (Vue) => {
|
||||||
Vue.component("p-alert", PAlert);
|
Vue.component("p-notify", PNotify);
|
||||||
Vue.component("p-navigation", PNavigation);
|
Vue.component("p-navigation", PNavigation);
|
||||||
Vue.component("p-loading-bar", PLoadingBar);
|
Vue.component("p-loading-bar", PLoadingBar);
|
||||||
Vue.component("p-photo-details", PPhotoDetails);
|
Vue.component("p-photo-details", PPhotoDetails);
|
||||||
|
|
|
@ -57,10 +57,6 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted () {
|
mounted () {
|
||||||
const overlay = document.createElement("div");
|
|
||||||
overlay.id = 'p-busy-overlay';
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
let stackSize = 0;
|
let stackSize = 0;
|
||||||
|
|
||||||
this.$event.subscribe('ajax.start', function () {
|
this.$event.subscribe('ajax.start', function () {
|
||||||
|
@ -68,7 +64,6 @@
|
||||||
|
|
||||||
if(stackSize === 1) {
|
if(stackSize === 1) {
|
||||||
this.start();
|
this.start();
|
||||||
document.getElementById('p-busy-overlay').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
|
@ -77,7 +72,6 @@
|
||||||
|
|
||||||
if (stackSize === 0) {
|
if (stackSize === 0) {
|
||||||
this.done();
|
this.done();
|
||||||
document.getElementById('p-busy-overlay').style.display = 'none';
|
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="p-navigation">
|
<div id="p-navigation">
|
||||||
<v-toolbar dark scroll-off-screen color="grey darken-3" class="hidden-lg-and-up p-navigation-small" @click.stop="showNavigation()">
|
<v-toolbar dark scroll-off-screen color="grey darken-3" class="hidden-lg-and-up p-navigation-small"
|
||||||
|
@click.stop="showNavigation()">
|
||||||
<v-toolbar-side-icon class="p-navigation-show"></v-toolbar-side-icon>
|
<v-toolbar-side-icon class="p-navigation-show"></v-toolbar-side-icon>
|
||||||
|
|
||||||
<v-toolbar-title class="p-navigation-title">{{ $router.currentRoute.meta.area }}</v-toolbar-title>
|
<v-toolbar-title class="p-navigation-title">{{ $router.currentRoute.meta.area }}</v-toolbar-title>
|
||||||
|
@ -58,13 +59,13 @@
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile :to="{ name: 'Photos', query: { q: 'mono:true' }}" :exact="true" @click="">
|
<v-list-tile :to="{name: 'photos', query: { q: 'mono:true' }}" :exact="true" @click="">
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>Monochrome</v-list-tile-title>
|
<v-list-tile-title>Monochrome</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile :to="{ name: 'Photos', query: { q: 'chroma:50' }}" :exact="true" @click="">
|
<v-list-tile :to="{name: 'photos', query: { q: 'chroma:50' }}" :exact="true" @click="">
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>Vibrant</v-list-tile-title>
|
<v-list-tile-title>Vibrant</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
|
@ -88,7 +89,7 @@
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile @click.stop="$alert.warning('Work in progress')">
|
<v-list-tile @click.stop="$notify.warning('Work in progress')">
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>Work in progress...</v-list-tile-title>
|
<v-list-tile-title>Work in progress...</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
|
@ -125,7 +126,7 @@
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile to="/events" @click="" class="p-navigation-events">
|
<!-- v-list-tile to="/events" @click="" class="p-navigation-events">
|
||||||
<v-list-tile-action>
|
<v-list-tile-action>
|
||||||
<v-icon>date_range</v-icon>
|
<v-icon>date_range</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
|
@ -133,9 +134,9 @@
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>Events</v-list-tile-title>
|
<v-list-tile-title>Events</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile -->
|
||||||
|
|
||||||
<v-list-tile to="/people" @click="" class="p-navigation-people">
|
<!-- v-list-tile to="/people" @click="" class="p-navigation-people">
|
||||||
<v-list-tile-action>
|
<v-list-tile-action>
|
||||||
<v-icon>people</v-icon>
|
<v-icon>people</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
|
@ -143,9 +144,9 @@
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>People</v-list-tile-title>
|
<v-list-tile-title>People</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile -->
|
||||||
|
|
||||||
<v-list-tile to="/library" @click="" class="p-navigation-library">
|
<v-list-tile to="/library" @click="" class="p-navigation-library" v-if="session.auth || isPublic">
|
||||||
<v-list-tile-action>
|
<v-list-tile-action>
|
||||||
<v-icon>camera_roll</v-icon>
|
<v-icon>camera_roll</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
|
@ -155,7 +156,7 @@
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile to="/settings" @click="" class="p-navigation-settings">
|
<v-list-tile to="/settings" @click="" class="p-navigation-settings" v-if="session.auth || isPublic">
|
||||||
<v-list-tile-action>
|
<v-list-tile-action>
|
||||||
<v-icon>settings</v-icon>
|
<v-icon>settings</v-icon>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
|
@ -164,6 +165,26 @@
|
||||||
<v-list-tile-title>Settings</v-list-tile-title>
|
<v-list-tile-title>Settings</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
|
<v-list-tile @click="logout" class="p-navigation-logout" v-if="!isPublic && session.auth">
|
||||||
|
<v-list-tile-action>
|
||||||
|
<v-icon>power_settings_new</v-icon>
|
||||||
|
</v-list-tile-action>
|
||||||
|
|
||||||
|
<v-list-tile-content>
|
||||||
|
<v-list-tile-title>Logout</v-list-tile-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
</v-list-tile>
|
||||||
|
|
||||||
|
<v-list-tile to="/login" @click="" class="p-navigation-login" v-if="!isPublic && !session.auth">
|
||||||
|
<v-list-tile-action>
|
||||||
|
<v-icon>lock</v-icon>
|
||||||
|
</v-list-tile-action>
|
||||||
|
|
||||||
|
<v-list-tile-content>
|
||||||
|
<v-list-tile-title>Login</v-list-tile-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
</v-list-tile>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -178,13 +199,18 @@
|
||||||
return {
|
return {
|
||||||
drawer: null,
|
drawer: null,
|
||||||
mini: mini,
|
mini: mini,
|
||||||
|
session: this.$session,
|
||||||
|
isPublic: this.$config.getValue("public"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showNavigation: function () {
|
showNavigation: function () {
|
||||||
this.drawer = true;
|
this.drawer = true;
|
||||||
this.mini = false;
|
this.mini = false;
|
||||||
}
|
},
|
||||||
|
logout() {
|
||||||
|
this.$session.logout();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
id="p-alert"
|
id="p-notify"
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:color="color"
|
:color="color"
|
||||||
:timeout="0"
|
:timeout="0"
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
import Event from 'pubsub-js';
|
import Event from 'pubsub-js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'p-alert',
|
name: 'p-notify',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
text: '',
|
text: '',
|
||||||
|
@ -37,30 +37,36 @@
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.subscriptionId = Event.subscribe('alert', this.handleAlertEvent);
|
this.subscriptionId = Event.subscribe('notify', this.eventHandler);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
Event.unsubscribe(this.subscriptionId);
|
Event.unsubscribe(this.subscriptionId);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleAlertEvent: function (ev, message) {
|
eventHandler: function (ev, data) {
|
||||||
const type = ev.split('.')[1];
|
const type = ev.split('.')[1];
|
||||||
|
|
||||||
|
// get message from data object
|
||||||
|
let m = data.msg;
|
||||||
|
|
||||||
|
// first letter uppercase
|
||||||
|
m = m.replace(/^./, m[0].toUpperCase());
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'warning':
|
case 'warning':
|
||||||
this.addWarningMessage(message);
|
this.addWarningMessage(m);
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
this.addErrorMessage(message);
|
this.addErrorMessage(m);
|
||||||
break;
|
break;
|
||||||
case 'success':
|
case 'success':
|
||||||
this.addSuccessMessage(message);
|
this.addSuccessMessage(m);
|
||||||
break;
|
break;
|
||||||
case 'info':
|
case 'info':
|
||||||
this.addInfoMessage(message);
|
this.addInfoMessage(m);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
alert(message);
|
alert(m);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -86,9 +92,15 @@
|
||||||
this.lastMessageId++;
|
this.lastMessageId++;
|
||||||
this.lastMessage = message;
|
this.lastMessage = message;
|
||||||
|
|
||||||
const alert = {'id': this.lastMessageId, 'color': color, 'textColor': textColor, 'delay': delay, 'msg': message};
|
const m = {
|
||||||
|
'id': this.lastMessageId,
|
||||||
|
'color': color,
|
||||||
|
'textColor': textColor,
|
||||||
|
'delay': delay,
|
||||||
|
'msg': message
|
||||||
|
};
|
||||||
|
|
||||||
this.messages.push(alert);
|
this.messages.push(m);
|
||||||
|
|
||||||
if(!this.visible) {
|
if(!this.visible) {
|
||||||
this.show();
|
this.show();
|
|
@ -102,8 +102,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Event from "pubsub-js";
|
import Api from "common/api";
|
||||||
import axios from "axios";
|
import Notify from "common/notify";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'p-photo-clipboard',
|
name: 'p-photo-clipboard',
|
||||||
|
@ -125,59 +125,44 @@
|
||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
},
|
},
|
||||||
batchPrivate() {
|
batchPrivate() {
|
||||||
Event.publish("ajax.start");
|
|
||||||
|
|
||||||
const ctx = this;
|
const ctx = this;
|
||||||
|
|
||||||
axios.post("/api/v1/batch/photos/private", {"ids": this.selection}).then(function () {
|
Api.post("batch/photos/private", {"ids": this.selection}).then(function () {
|
||||||
Event.publish("ajax.end");
|
Notify.success("Toggled private flag");
|
||||||
Event.publish("alert.success", "Toggled private flag");
|
|
||||||
ctx.clearClipboard();
|
ctx.clearClipboard();
|
||||||
ctx.refresh();
|
ctx.refresh();
|
||||||
}).catch(() => {
|
|
||||||
Event.publish("ajax.end");
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
batchStory() {
|
batchStory() {
|
||||||
Event.publish("ajax.start");
|
|
||||||
|
|
||||||
const ctx = this;
|
const ctx = this;
|
||||||
|
|
||||||
axios.post("/api/v1/batch/photos/story", {"ids": this.selection}).then(function () {
|
Api.post("batch/photos/story", {"ids": this.selection}).then(function () {
|
||||||
Event.publish("ajax.end");
|
Notify.success("Toggled story flag");
|
||||||
Event.publish("alert.success", "Toggled story flag");
|
|
||||||
ctx.clearClipboard();
|
ctx.clearClipboard();
|
||||||
ctx.refresh();
|
ctx.refresh();
|
||||||
}).catch(() => {
|
|
||||||
Event.publish("ajax.end");
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
batchDelete() {
|
batchDelete() {
|
||||||
this.dialog.delete = false;
|
this.dialog.delete = false;
|
||||||
|
|
||||||
Event.publish("ajax.start");
|
|
||||||
|
|
||||||
const ctx = this;
|
const ctx = this;
|
||||||
|
|
||||||
axios.post("/api/v1/batch/photos/delete", {"ids": this.selection}).then(function () {
|
Api.post("batch/photos/delete", {"ids": this.selection}).then(function () {
|
||||||
Event.publish("ajax.end");
|
Notify.success("Photos deleted");
|
||||||
Event.publish("alert.success", "Photos deleted");
|
|
||||||
ctx.clearClipboard();
|
ctx.clearClipboard();
|
||||||
ctx.refresh();
|
ctx.refresh();
|
||||||
}).catch(() => {
|
|
||||||
Event.publish("ajax.end");
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
batchTag() {
|
batchTag() {
|
||||||
this.$alert.warning("Not implemented yet");
|
Notify.warning("Not implemented yet");
|
||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
},
|
},
|
||||||
batchAlbum() {
|
batchAlbum() {
|
||||||
this.$alert.warning("Not implemented yet");
|
Notify.warning("Not implemented yet");
|
||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
},
|
},
|
||||||
batchDownload() {
|
batchDownload() {
|
||||||
this.$alert.warning("Not implemented yet");
|
Notify.warning("Not implemented yet");
|
||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
},
|
},
|
||||||
openDocs() {
|
openDocs() {
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
>
|
>
|
||||||
<v-hover>
|
<v-hover>
|
||||||
<v-card tile slot-scope="{ hover }"
|
<v-card tile slot-scope="{ hover }"
|
||||||
:class="$clipboard.has(photo) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'">
|
:class="$clipboard.has(photo) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'"
|
||||||
|
:title="photo.PhotoTitle">
|
||||||
<v-img :src="photo.getThumbnailUrl('tile_224')"
|
<v-img :src="photo.getThumbnailUrl('tile_224')"
|
||||||
aspect-ratio="1"
|
aspect-ratio="1"
|
||||||
class="grey lighten-2"
|
class="grey lighten-2"
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
>
|
>
|
||||||
<v-hover>
|
<v-hover>
|
||||||
<v-card tile slot-scope="{ hover }"
|
<v-card tile slot-scope="{ hover }"
|
||||||
:class="$clipboard.has(photo) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'">
|
:class="$clipboard.has(photo) ? 'elevation-10 ma-0' : 'elevation-0 ma-1'"
|
||||||
|
:title="photo.PhotoTitle">
|
||||||
<v-img :src="photo.getThumbnailUrl('tile_500')"
|
<v-img :src="photo.getThumbnailUrl('tile_500')"
|
||||||
aspect-ratio="1"
|
aspect-ratio="1"
|
||||||
class="grey lighten-2"
|
class="grey lighten-2"
|
||||||
|
|
5
frontend/src/i18n/translations.json
Normal file
5
frontend/src/i18n/translations.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"en": {
|
||||||
|
"theme": "Theme"
|
||||||
|
}
|
||||||
|
}
|
56
frontend/src/model/settings.js
Normal file
56
frontend/src/model/settings.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import Api from "common/api";
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
constructor(values) {
|
||||||
|
this.__originalValues = {};
|
||||||
|
|
||||||
|
if (!values) {
|
||||||
|
values = {
|
||||||
|
theme: "dark",
|
||||||
|
language: "en",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("config values", values);
|
||||||
|
|
||||||
|
this.setValues(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValues(values) {
|
||||||
|
if(!values) return;
|
||||||
|
|
||||||
|
for(let key in values) {
|
||||||
|
if(values.hasOwnProperty(key) && key !== "__originalValues") {
|
||||||
|
this[key] = values[key];
|
||||||
|
this.__originalValues[key] = values[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValues() {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for(let key in this.__originalValues) {
|
||||||
|
if(this.__originalValues.hasOwnProperty(key) && key !== "__originalValues") {
|
||||||
|
result[key] = this[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
return Api.get("settings").then((response) => {
|
||||||
|
return Promise.resolve(this.setValues(response.data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
|
||||||
|
return Api.post("settings", this.getValues()).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings;
|
|
@ -4,7 +4,7 @@ import Api from "common/api";
|
||||||
|
|
||||||
class User extends Abstract {
|
class User extends Abstract {
|
||||||
getEntityName() {
|
getEntityName() {
|
||||||
return this.userFirstName + " " + this.userLastName;
|
return this.FirstName + " " + this.LastName;
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() {
|
getId() {
|
||||||
|
|
|
@ -125,7 +125,7 @@
|
||||||
},
|
},
|
||||||
openAlbum(index) {
|
openAlbum(index) {
|
||||||
const album = this.results[index];
|
const album = this.results[index];
|
||||||
this.$router.push({name: 'Photos', query: {q: "album:" + album.AlbumSlug}});
|
this.$router.push({name: "photos", query: {q: "album:" + album.AlbumSlug}});
|
||||||
},
|
},
|
||||||
loadMore() {
|
loadMore() {
|
||||||
if (this.scrollDisabled) return;
|
if (this.scrollDisabled) return;
|
||||||
|
@ -147,7 +147,7 @@
|
||||||
this.scrollDisabled = (response.models.length < this.pageSize);
|
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||||
|
|
||||||
if (this.scrollDisabled) {
|
if (this.scrollDisabled) {
|
||||||
this.$alert.info('All ' + this.results.length + ' albums loaded');
|
this.$notify.info('All ' + this.results.length + ' albums loaded');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -201,9 +201,9 @@
|
||||||
this.scrollDisabled = (response.models.length < this.pageSize);
|
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||||
|
|
||||||
if (this.scrollDisabled) {
|
if (this.scrollDisabled) {
|
||||||
this.$alert.info(this.results.length + ' albums found');
|
this.$notify.info(this.results.length + ' albums found');
|
||||||
} else {
|
} else {
|
||||||
this.$alert.info('More than 20 albums found');
|
this.$notify.info('More than 20 albums found');
|
||||||
|
|
||||||
this.$nextTick(() => this.$emit("scrollRefresh"));
|
this.$nextTick(() => this.$emit("scrollRefresh"));
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,7 @@
|
||||||
const album = new Album({"AlbumName": name});
|
const album = new Album({"AlbumName": name});
|
||||||
|
|
||||||
album.save().then(() => {
|
album.save().then(() => {
|
||||||
this.$alert.success(name + " created");
|
this.$notify.success(name + " created");
|
||||||
|
|
||||||
this.filter.q = "";
|
this.filter.q = "";
|
||||||
this.lastFilter = {};
|
this.lastFilter = {};
|
||||||
|
|
|
@ -1,550 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-infinite-scroll="loadMore" infinite-scroll-disabled="loadMoreDisabled" infinite-scroll-distance="10">
|
|
||||||
<v-form ref="form" lazy-validation @submit="formChange" dense>
|
|
||||||
<v-toolbar flat color="blue-grey lighten-4">
|
|
||||||
<v-text-field class="pt-3 pr-3"
|
|
||||||
single-line
|
|
||||||
label="Search"
|
|
||||||
prepend-inner-icon="search"
|
|
||||||
clearable
|
|
||||||
color="blue-grey"
|
|
||||||
@click:clear="clearQuery"
|
|
||||||
v-model="query.q"
|
|
||||||
@keyup.enter.native="formChange"
|
|
||||||
></v-text-field>
|
|
||||||
<!-- v-btn @click="formChange" color="secondary">Create Filter</v-btn -->
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
|
|
||||||
<v-btn icon @click="advandedSearch = !advandedSearch">
|
|
||||||
<v-icon>{{ advandedSearch ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<v-card class="pt-1"
|
|
||||||
flat
|
|
||||||
color="blue-grey lighten-5"
|
|
||||||
v-show="advandedSearch">
|
|
||||||
<v-card-text>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex xs12 sm6 md3 pa-2>
|
|
||||||
<v-select @change="formChange"
|
|
||||||
label="Country"
|
|
||||||
flat solo hide-details
|
|
||||||
color="blue-grey"
|
|
||||||
item-value="LocCountryCode"
|
|
||||||
item-text="LocCountry"
|
|
||||||
v-model="query.country"
|
|
||||||
:items="options.countries">
|
|
||||||
</v-select>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex xs12 sm6 md3 pa-2>
|
|
||||||
<v-select @change="formChange"
|
|
||||||
label="Camera"
|
|
||||||
flat solo hide-details
|
|
||||||
color="blue-grey"
|
|
||||||
item-value="ID"
|
|
||||||
item-text="CameraModel"
|
|
||||||
v-model="query.camera"
|
|
||||||
:items="options.cameras">
|
|
||||||
</v-select>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex xs12 sm6 md3 pa-2>
|
|
||||||
<v-select @change="formChange"
|
|
||||||
label="View"
|
|
||||||
flat solo hide-details
|
|
||||||
color="blue-grey"
|
|
||||||
v-model="query.view"
|
|
||||||
:items="options.views">
|
|
||||||
</v-select>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex xs12 sm6 md3 pa-2>
|
|
||||||
<v-select @change="formChange"
|
|
||||||
label="Sort By"
|
|
||||||
flat solo hide-details
|
|
||||||
color="blue-grey"
|
|
||||||
v-model="query.order"
|
|
||||||
:items="options.sorting">
|
|
||||||
</v-select>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-form>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-layout wrap>
|
|
||||||
<v-flex xs12>
|
|
||||||
<h1>
|
|
||||||
<v-text-field label="Album name*" required value="South Africa 2018" solo></v-text-field>
|
|
||||||
</h1>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex xs12>
|
|
||||||
<v-textarea label="Description" value="Nice photo collection of the last vacation"
|
|
||||||
solo></v-textarea>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
<p> In case you want to add photos to this album
|
|
||||||
--> go to the photos view --> select all photos you want to add
|
|
||||||
--> click add to album in the bottom right menu and select this album.</p>
|
|
||||||
<p> In case you want to remove photos from this album
|
|
||||||
--> select all photos you want to remove
|
|
||||||
--> click remove in the bottom right menu.</p>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<v-container fluid>
|
|
||||||
<v-speed-dial
|
|
||||||
fixed
|
|
||||||
bottom
|
|
||||||
right
|
|
||||||
direction="top"
|
|
||||||
open-on-hover
|
|
||||||
transition="slide-y-reverse-transition"
|
|
||||||
style="right: 8px; bottom: 8px;"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
slot="activator"
|
|
||||||
color="grey darken-2"
|
|
||||||
dark
|
|
||||||
fab
|
|
||||||
>
|
|
||||||
<v-icon>menu</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
fab
|
|
||||||
dark
|
|
||||||
small
|
|
||||||
color="deep-purple lighten-2"
|
|
||||||
>
|
|
||||||
<v-icon>favorite</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
fab
|
|
||||||
dark
|
|
||||||
small
|
|
||||||
color="cyan accent-4"
|
|
||||||
>
|
|
||||||
<v-icon>youtube_searched_for</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
fab
|
|
||||||
dark
|
|
||||||
small
|
|
||||||
color="teal accent-4"
|
|
||||||
>
|
|
||||||
<v-icon>save</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
fab
|
|
||||||
dark
|
|
||||||
small
|
|
||||||
color="yellow accent-4"
|
|
||||||
>
|
|
||||||
<v-icon>create_new_folder</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
fab
|
|
||||||
dark
|
|
||||||
small
|
|
||||||
color="delete"
|
|
||||||
>
|
|
||||||
<v-icon>delete</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-speed-dial>
|
|
||||||
<v-data-table
|
|
||||||
:headers="listColumns"
|
|
||||||
:items="results"
|
|
||||||
hide-actions
|
|
||||||
class="elevation-1"
|
|
||||||
v-if="query.view === 'list'"
|
|
||||||
select-all
|
|
||||||
disable-initial-sort
|
|
||||||
item-key="ID"
|
|
||||||
v-model="selected"
|
|
||||||
:no-data-text="'No photos matched your search'"
|
|
||||||
>
|
|
||||||
<template slot="items" slot-scope="props">
|
|
||||||
<td>
|
|
||||||
<v-checkbox
|
|
||||||
v-model="props.selected"
|
|
||||||
primary
|
|
||||||
hide-details
|
|
||||||
></v-checkbox>
|
|
||||||
</td>
|
|
||||||
<td>{{ props.item.PhotoTitle }}</td>
|
|
||||||
<td>{{ props.item.TakenAt | luxon:format('dd/MM/yyyy hh:mm:ss') }}</td>
|
|
||||||
<td>{{ props.item.LocCity }}</td>
|
|
||||||
<td>{{ props.item.LocCountry }}</td>
|
|
||||||
<td>{{ props.item.CameraModel }}</td>
|
|
||||||
<td>{{ props.item.PhotoFavorite ? 'Yes' : 'No' }}</td>
|
|
||||||
</template>
|
|
||||||
</v-data-table>
|
|
||||||
|
|
||||||
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'details'">
|
|
||||||
<v-card v-if="results.length === 0">
|
|
||||||
<v-card-title primary-title>
|
|
||||||
<div>
|
|
||||||
<h3 class="title mb-3">No photos matched your search</h3>
|
|
||||||
<div>Try using other terms and search options such as category, country and camera.</div>
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
</v-card>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex
|
|
||||||
v-for="(photo, index) in results"
|
|
||||||
:key="photo.ID"
|
|
||||||
xs12 sm6 md4 lg3 d-flex
|
|
||||||
>
|
|
||||||
<v-hover>
|
|
||||||
<v-card tile slot-scope="{ hover }"
|
|
||||||
:dark="photo.selected"
|
|
||||||
:class="photo.selected ? 'elevation-14 ma-1' : 'elevation-2 ma-2'">
|
|
||||||
<v-img
|
|
||||||
:src="photo.getThumbnailUrl('tile_500')"
|
|
||||||
aspect-ratio="1"
|
|
||||||
v-bind:class="{ selected: photo.selected }"
|
|
||||||
style="cursor: pointer"
|
|
||||||
class="grey lighten-2"
|
|
||||||
@click="openPhoto(index)"
|
|
||||||
|
|
||||||
>
|
|
||||||
<v-layout
|
|
||||||
slot="placeholder"
|
|
||||||
fill-height
|
|
||||||
align-center
|
|
||||||
justify-center
|
|
||||||
ma-0
|
|
||||||
>
|
|
||||||
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
|
|
||||||
</v-layout>
|
|
||||||
|
|
||||||
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
|
|
||||||
:ripple="false" style="right: 4px; bottom: 4px;"
|
|
||||||
@click.stop.prevent="selectPhoto(photo)">
|
|
||||||
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
|
|
||||||
<v-icon v-else color="white">check_box_outline_blank</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
|
|
||||||
:ripple="false" style="bottom: 4px; left: 4px"
|
|
||||||
@click.stop.prevent="likePhoto(photo)">
|
|
||||||
<v-icon v-if="photo.PhotoFavorite" color="white">favorite
|
|
||||||
</v-icon>
|
|
||||||
<v-icon v-else color="white">favorite_border</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-img>
|
|
||||||
|
|
||||||
|
|
||||||
<v-card-title primary-title class="pa-3">
|
|
||||||
<div>
|
|
||||||
<h3 class="subheading mb-2" :title="photo.PhotoTitle">{{ photo.PhotoTitle |
|
|
||||||
truncate(80) }}</h3>
|
|
||||||
<div class="caption">
|
|
||||||
<v-icon size="14">date_range</v-icon>
|
|
||||||
{{ photo.TakenAt | luxon:format('dd/MM/yyyy hh:mm:ss') }}
|
|
||||||
<br/>
|
|
||||||
<v-icon size="14">photo_camera</v-icon>
|
|
||||||
{{ photo.getCamera() }}
|
|
||||||
<br/>
|
|
||||||
<v-icon size="14">location_on</v-icon>
|
|
||||||
<span :title="photo.getFullLocation()">{{ photo.getLocation() }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
</v-card>
|
|
||||||
</v-hover>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<v-container grid-list-xs fluid class="pa-0" v-if="query.view === 'tiles'">
|
|
||||||
<v-card v-if="results.length === 0">
|
|
||||||
<v-card-title primary-title>
|
|
||||||
<div>
|
|
||||||
<h3 class="headline mb-3">No photos matched your search</h3>
|
|
||||||
<div>Try using other terms and search options such as category, country and camera.</div>
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
</v-card>
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex
|
|
||||||
v-for="(photo, index) in results"
|
|
||||||
:key="photo.ID"
|
|
||||||
xs12 sm6 md3 lg2 d-flex
|
|
||||||
v-bind:class="{ selected: photo.selected }"
|
|
||||||
>
|
|
||||||
<v-hover>
|
|
||||||
<v-card tile slot-scope="{ hover }"
|
|
||||||
:dark="photo.selected"
|
|
||||||
:class="photo.selected ? 'elevation-14 ma-1' : hover ? 'elevation-6 ma-2' : 'elevation-2 ma-2'">
|
|
||||||
<v-img :src="photo.getThumbnailUrl('tile_500')"
|
|
||||||
aspect-ratio="1"
|
|
||||||
class="grey lighten-2"
|
|
||||||
style="cursor: pointer"
|
|
||||||
@click="openPhoto(index)"
|
|
||||||
>
|
|
||||||
<v-layout
|
|
||||||
slot="placeholder"
|
|
||||||
fill-height
|
|
||||||
align-center
|
|
||||||
justify-center
|
|
||||||
ma-0
|
|
||||||
>
|
|
||||||
<v-progress-circular indeterminate
|
|
||||||
color="grey lighten-5"></v-progress-circular>
|
|
||||||
</v-layout>
|
|
||||||
|
|
||||||
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
|
|
||||||
:ripple="false" style="right: 4px; bottom: 4px;"
|
|
||||||
@click.stop.prevent="selectPhoto(photo)">
|
|
||||||
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
|
|
||||||
<v-icon v-else color="white">check_box_outline_blank</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
|
|
||||||
:ripple="false" style="bottom: 4px; left: 4px"
|
|
||||||
@click.stop.prevent="likePhoto(photo)">
|
|
||||||
<v-icon v-if="photo.PhotoFavorite" color="white">favorite</v-icon>
|
|
||||||
<v-icon v-else color="white">favorite_border</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-img>
|
|
||||||
|
|
||||||
</v-card>
|
|
||||||
</v-hover>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
<v-snackbar
|
|
||||||
v-model="snackbarVisible"
|
|
||||||
bottom
|
|
||||||
:timeout="0"
|
|
||||||
>
|
|
||||||
{{ snackbarText }}
|
|
||||||
<v-btn
|
|
||||||
class="pr-0"
|
|
||||||
color="primary"
|
|
||||||
icon
|
|
||||||
flat
|
|
||||||
@click="clearSelection()"
|
|
||||||
>
|
|
||||||
<v-icon>close</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-snackbar>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Photo from 'model/photo';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'browse',
|
|
||||||
props: {},
|
|
||||||
data() {
|
|
||||||
dialog: false;
|
|
||||||
const query = this.$route.query;
|
|
||||||
const order = query['order'] ? query['order'] : 'newest';
|
|
||||||
const camera = query['camera'] ? parseInt(query['camera']) : 0;
|
|
||||||
const q = query['q'] ? query['q'] : '';
|
|
||||||
const country = query['country'] ? query['country'] : '';
|
|
||||||
const view = query['view'] ? query['view'] : 'tiles';
|
|
||||||
const cameras = [{ID: 0, CameraModel: 'All Cameras'}].concat(this.$config.getValue('cameras'));
|
|
||||||
const countries = [{
|
|
||||||
LocCountryCode: '',
|
|
||||||
LocCountry: 'All Countries'
|
|
||||||
}].concat(this.$config.getValue('countries'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
'snackbarVisible': false,
|
|
||||||
'snackbarText': '',
|
|
||||||
'advandedSearch': false,
|
|
||||||
'window': {
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
'results': [],
|
|
||||||
'query': {
|
|
||||||
view: view,
|
|
||||||
country: country,
|
|
||||||
camera: camera,
|
|
||||||
order: order,
|
|
||||||
q: q,
|
|
||||||
},
|
|
||||||
'options': {
|
|
||||||
'categories': [
|
|
||||||
{value: '', text: 'All Categories'},
|
|
||||||
{value: 'airport', text: 'Airport'},
|
|
||||||
{value: 'amenity', text: 'Amenity'},
|
|
||||||
{value: 'building', text: 'Building'},
|
|
||||||
{value: 'historic', text: 'Historic'},
|
|
||||||
{value: 'shop', text: 'Shop'},
|
|
||||||
{value: 'tourism', text: 'Tourism'},
|
|
||||||
],
|
|
||||||
'views': [
|
|
||||||
{value: 'tiles', text: 'Tiles'},
|
|
||||||
{value: 'details', text: 'Details'},
|
|
||||||
{value: 'list', text: 'List'},
|
|
||||||
],
|
|
||||||
'countries': countries,
|
|
||||||
'cameras': cameras,
|
|
||||||
'sorting': [
|
|
||||||
{value: 'newest', text: 'Newest first'},
|
|
||||||
{value: 'oldest', text: 'Oldest first'},
|
|
||||||
{value: 'imported', text: 'Recently imported'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'listColumns': [
|
|
||||||
{text: 'Title', value: 'PhotoTitle'},
|
|
||||||
{text: 'Taken At', value: 'TakenAt'},
|
|
||||||
{text: 'City', value: 'LocCity'},
|
|
||||||
{text: 'Country', value: 'LocCountry'},
|
|
||||||
{text: 'Camera', value: 'CameraModel'},
|
|
||||||
{text: 'Favorite', value: 'PhotoFavorite'},
|
|
||||||
],
|
|
||||||
'view': view,
|
|
||||||
'loadMoreDisabled': true,
|
|
||||||
'pageSize': 60,
|
|
||||||
'offset': 0,
|
|
||||||
'lastQuery': {},
|
|
||||||
'submitTimeout': false,
|
|
||||||
'selected': [],
|
|
||||||
'dialog': false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
window.removeEventListener('resize', this.handleResize)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleResize() {
|
|
||||||
this.window.width = window.innerWidth;
|
|
||||||
this.window.height = window.innerHeight;
|
|
||||||
},
|
|
||||||
clearSelection() {
|
|
||||||
for (let i = 0; i < this.selected.length; i++) {
|
|
||||||
this.selected[i].selected = false;
|
|
||||||
}
|
|
||||||
this.selected = [];
|
|
||||||
this.updateSnackbar();
|
|
||||||
},
|
|
||||||
updateSnackbar(text) {
|
|
||||||
if (!text) text = "";
|
|
||||||
|
|
||||||
this.snackbarText = text;
|
|
||||||
|
|
||||||
this.snackbarVisible = this.snackbarText !== "";
|
|
||||||
},
|
|
||||||
showSnackbar() {
|
|
||||||
this.snackbarVisible = this.snackbarText !== "";
|
|
||||||
},
|
|
||||||
hideSnackbar() {
|
|
||||||
this.snackbarVisible = false;
|
|
||||||
},
|
|
||||||
selectPhoto(photo, ev) {
|
|
||||||
if (photo.selected) {
|
|
||||||
for (let i = 0; i < this.selected.length; i++) {
|
|
||||||
if (this.selected[i].id === photo.id) {
|
|
||||||
this.selected.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
photo.selected = false;
|
|
||||||
} else {
|
|
||||||
this.selected.push(photo);
|
|
||||||
photo.selected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selected.length > 0) {
|
|
||||||
if (this.selected.length === 1) {
|
|
||||||
this.snackbarText = 'One photo selected';
|
|
||||||
} else {
|
|
||||||
this.snackbarText = this.selected.length + ' photos selected';
|
|
||||||
}
|
|
||||||
this.snackbarVisible = true;
|
|
||||||
} else {
|
|
||||||
this.snackbarText = '';
|
|
||||||
this.snackbarVisible = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
likePhoto(photo) {
|
|
||||||
photo.PhotoFavorite = !photo.PhotoFavorite;
|
|
||||||
photo.like(photo.PhotoFavorite);
|
|
||||||
},
|
|
||||||
deletePhoto(photo) {
|
|
||||||
this.$alert.success('Photo deleted');
|
|
||||||
},
|
|
||||||
formChange(event) {
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
clearQuery() {
|
|
||||||
this.query.q = '';
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
openPhoto(index) {
|
|
||||||
this.$viewer.show(this.results, index)
|
|
||||||
},
|
|
||||||
loadMore() {
|
|
||||||
if (this.loadMoreDisabled) return;
|
|
||||||
|
|
||||||
this.loadMoreDisabled = true;
|
|
||||||
|
|
||||||
this.offset += this.pageSize;
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
count: this.pageSize,
|
|
||||||
offset: this.offset,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(params, this.lastQuery);
|
|
||||||
|
|
||||||
Photo.search(params).then(response => {
|
|
||||||
this.results = this.results.concat(response.models);
|
|
||||||
|
|
||||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
|
||||||
|
|
||||||
if (this.loadMoreDisabled) {
|
|
||||||
this.$alert.info('All ' + this.results.length + ' photos loaded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
search() {
|
|
||||||
this.loadMoreDisabled = true;
|
|
||||||
|
|
||||||
// Don't query the same data more than once:197
|
|
||||||
if (JSON.stringify(this.lastQuery) === JSON.stringify(this.query)) return;
|
|
||||||
|
|
||||||
Object.assign(this.lastQuery, this.query);
|
|
||||||
|
|
||||||
this.offset = 0;
|
|
||||||
|
|
||||||
this.$router.replace({query: this.query});
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
count: this.pageSize,
|
|
||||||
offset: this.offset,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(params, this.query);
|
|
||||||
|
|
||||||
Photo.search(params).then(response => {
|
|
||||||
this.results = response.models;
|
|
||||||
|
|
||||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
|
||||||
|
|
||||||
if (this.loadMoreDisabled) {
|
|
||||||
this.$alert.info(this.results.length + ' photos found');
|
|
||||||
} else {
|
|
||||||
this.$alert.info('More than 50 photos found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeRouteLeave(to, from, next) {
|
|
||||||
next()
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
this.handleResize();
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,52 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-toolbar flat color="blue-grey lighten-4">
|
|
||||||
<h1>Import</h1>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-form>
|
|
||||||
<p class="md-subheading">
|
|
||||||
You have two possibilities to get your photos into photoprism.</p>
|
|
||||||
<h2>Import & Index</h2>
|
|
||||||
<p>Importing means the photos you upload are renamed (the naming schema you can define in settings), and moved to the originals folder sorted by year and month.
|
|
||||||
Additionally duplicates are removed and images get tagged and metadata (like location, camera model etc.) will be extracted. In case you have not supported file types
|
|
||||||
(e.g. videos) within the folder you import --> those are ignored. </p>
|
|
||||||
<v-btn color="success" @click="$refs.inputUpload.click()" type="file" class="importbtn" disabled>Import & Index</v-btn>
|
|
||||||
<input v-show="false" ref="inputUpload" type="file">
|
|
||||||
<v-flex xs12 sm6 offset-sm3>
|
|
||||||
<v-card class="card">
|
|
||||||
<v-card-title primary-title>
|
|
||||||
<div>
|
|
||||||
<div>598 JPEG and 432 RAW files found</div>
|
|
||||||
</div>
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat color="success">Start</v-btn>
|
|
||||||
<v-btn flat color="success">Cancel</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-form>
|
|
||||||
</v-container>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-form>
|
|
||||||
<h2>Index</h2>
|
|
||||||
<p>In case you already have a nice folder structure you can only index the photos. Therefore in settings you need to set the base directory to the directory your photos
|
|
||||||
are in. The index functionality will then just tag the images and extract the metadata.
|
|
||||||
</p>
|
|
||||||
<v-btn color="success" @click="$refs.inputUpload.click()" type="file" class="importbtn" disabled>Index</v-btn>
|
|
||||||
</v-form>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'import',
|
|
||||||
props: {},
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,57 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-toolbar flat color="blue-grey lighten-4">
|
|
||||||
<h1>Import</h1>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-form>
|
|
||||||
<p class="md-subheading">
|
|
||||||
You have two possibilities to get your photos into photoprism.</p>
|
|
||||||
<h2>Import & Index</h2>
|
|
||||||
<p>Importing means the photos you upload are renamed (the naming schema you can define in settings), and moved to the originals folder sorted by year and month.
|
|
||||||
Additionally duplicates are removed and images get tagged and metadata (like location, camera model etc.) will be extracted. In case you have not supported file types
|
|
||||||
(e.g. videos) within the folder you import --> those are ignored. </p>
|
|
||||||
<v-btn color="success" @click="$refs.inputUpload.click()" type="file" class="importbtn" disabled>Import & Index</v-btn>
|
|
||||||
<input v-show="false" ref="inputUpload" type="file">
|
|
||||||
<v-flex xs12 sm6 offset-sm3>
|
|
||||||
<v-card class="card">
|
|
||||||
<v-card-title primary-title>
|
|
||||||
<div>
|
|
||||||
<div>Processing image 360 from 1030</div>
|
|
||||||
</div>
|
|
||||||
<v-progress-linear
|
|
||||||
background-color="pink lighten-3"
|
|
||||||
color="pink lighten-1"
|
|
||||||
value="35"
|
|
||||||
></v-progress-linear>
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat color="success" disabled>Start</v-btn>
|
|
||||||
<v-btn flat color="success">Cancel</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-form>
|
|
||||||
</v-container>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-form>
|
|
||||||
<h2>Index</h2>
|
|
||||||
<p>In case you already have a nice folder structure you can only index the photos. Therefore in settings you need to set the base directory to the directory your photos
|
|
||||||
are in. The index functionality will then just tag the images and extract the metadata.
|
|
||||||
</p>
|
|
||||||
<v-btn color="success" @click="$refs.inputUpload.click()" type="file" class="importbtn" disabled>Index</v-btn>
|
|
||||||
</v-form>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'import',
|
|
||||||
props: {},
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -86,7 +86,7 @@
|
||||||
staticFilter: Object
|
staticFilter: Object
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route' () {
|
'$route'() {
|
||||||
const query = this.$route.query;
|
const query = this.$route.query;
|
||||||
|
|
||||||
this.filter.q = query['q'];
|
this.filter.q = query['q'];
|
||||||
|
@ -121,7 +121,7 @@
|
||||||
},
|
},
|
||||||
openLabel(index) {
|
openLabel(index) {
|
||||||
const label = this.results[index];
|
const label = this.results[index];
|
||||||
this.$router.push({name: 'Photos', query: {q: "label:" + label.LabelSlug}});
|
this.$router.push({name: "photos", query: {q: "label:" + label.LabelSlug}});
|
||||||
},
|
},
|
||||||
loadMore() {
|
loadMore() {
|
||||||
if (this.scrollDisabled) return;
|
if (this.scrollDisabled) return;
|
||||||
|
@ -143,7 +143,7 @@
|
||||||
this.scrollDisabled = (response.models.length < this.pageSize);
|
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||||
|
|
||||||
if (this.scrollDisabled) {
|
if (this.scrollDisabled) {
|
||||||
this.$alert.info('All ' + this.results.length + ' labels loaded');
|
this.$notify.info('All ' + this.results.length + ' labels loaded');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -197,9 +197,9 @@
|
||||||
this.scrollDisabled = (response.models.length < this.pageSize);
|
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||||
|
|
||||||
if (this.scrollDisabled) {
|
if (this.scrollDisabled) {
|
||||||
this.$alert.info(this.results.length + ' labels found');
|
this.$notify.info(this.results.length + ' labels found');
|
||||||
} else {
|
} else {
|
||||||
this.$alert.info('More than 20 labels found');
|
this.$notify.info('More than 20 labels found');
|
||||||
|
|
||||||
this.$nextTick(() => this.$emit("scrollRefresh"));
|
this.$nextTick(() => this.$emit("scrollRefresh"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<v-form ref="form" class="p-photo-import" lazy-validation @submit.prevent="submit" dense>
|
<v-form ref="form" class="p-photo-import" lazy-validation @submit.prevent="submit" dense>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<p class="subheading">
|
<p class="subheading">
|
||||||
<span v-if="busy">Importing files from directory...</span>
|
<span v-if="fileName">Importing {{ fileName }}...</span>
|
||||||
|
<span v-else-if="busy">Importing files from directory...</span>
|
||||||
<span v-else-if="completed">Done.</span>
|
<span v-else-if="completed">Done.</span>
|
||||||
<span v-else>Press button to import photos from directory...</span>
|
<span v-else>Press button to import photos from directory...</span>
|
||||||
</p>
|
</p>
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
<v-btn
|
<v-btn
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
color="blue-grey"
|
color="blue-grey"
|
||||||
class="white--text ml-0"
|
class="white--text ml-0 mt-2"
|
||||||
depressed
|
depressed
|
||||||
@click.stop="startImport()"
|
@click.stop="startImport()"
|
||||||
>
|
>
|
||||||
|
@ -26,7 +27,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import Api from "common/api";
|
||||||
|
import Axios from "axios";
|
||||||
|
import Notify from "common/notify";
|
||||||
import Event from "pubsub-js";
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -36,31 +39,75 @@
|
||||||
started: false,
|
started: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
|
subscriptionId: '',
|
||||||
|
fileName: '',
|
||||||
|
source: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
console.log("SUBMIT");
|
// DO NOTHING
|
||||||
},
|
},
|
||||||
startImport() {
|
startImport() {
|
||||||
|
this.source = Axios.CancelToken.source();
|
||||||
this.started = Date.now();
|
this.started = Date.now();
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.completed = 0;
|
this.completed = 0;
|
||||||
|
this.fileName = '';
|
||||||
this.$alert.info("Importing photos...");
|
|
||||||
|
|
||||||
const ctx = this;
|
const ctx = this;
|
||||||
|
Notify.blockUI();
|
||||||
|
|
||||||
axios.post('/api/v1/import').then(function () {
|
Api.post('import', {}, { cancelToken: this.source.token }).then(function () {
|
||||||
Event.publish("alert.success", "Import complete");
|
Notify.unblockUI();
|
||||||
ctx.busy = false;
|
ctx.busy = false;
|
||||||
ctx.completed = 100;
|
ctx.completed = 100;
|
||||||
}).catch(function () {
|
ctx.fileName = '';
|
||||||
Event.publish("alert.error", "Import failed");
|
}).catch(function (e) {
|
||||||
|
Notify.unblockUI();
|
||||||
|
|
||||||
|
if (Axios.isCancel(e)) {
|
||||||
|
// run in background
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify.error("Import failed");
|
||||||
|
|
||||||
ctx.busy = false;
|
ctx.busy = false;
|
||||||
ctx.completed = 0;
|
ctx.completed = 0;
|
||||||
|
ctx.fileName = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
handleEvent(ev, data) {
|
||||||
|
if(this.source) {
|
||||||
|
this.source.cancel('run in background');
|
||||||
|
this.source = null;
|
||||||
|
Notify.unblockUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = ev.split('.')[1];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'file':
|
||||||
|
this.busy = true;
|
||||||
|
this.completed = 0;
|
||||||
|
this.fileName = data.baseName;
|
||||||
|
break;
|
||||||
|
case 'completed':
|
||||||
|
this.busy = false;
|
||||||
|
this.completed = 100;
|
||||||
|
this.fileName = '';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.subscriptionId = Event.subscribe('import', this.handleEvent);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
Event.unsubscribe(this.subscriptionId);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
|
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<p class="subheading">
|
<p class="subheading">
|
||||||
<span v-if="busy">Re-indexing existing files and photos...</span>
|
<span v-if="fileName">Indexing {{ fileName }}...</span>
|
||||||
|
<span v-else-if="busy">Re-indexing existing files and photos...</span>
|
||||||
<span v-else-if="completed">Done.</span>
|
<span v-else-if="completed">Done.</span>
|
||||||
<span v-else>Press button to re-index existing files and photos...</span>
|
<span v-else>Press button to re-index existing files and photos...</span>
|
||||||
</p>
|
</p>
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
<v-btn
|
<v-btn
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
color="blue-grey"
|
color="blue-grey"
|
||||||
class="white--text ml-0"
|
class="white--text ml-0 mt-2"
|
||||||
depressed
|
depressed
|
||||||
@click.stop="startIndexing()"
|
@click.stop="startIndexing()"
|
||||||
>
|
>
|
||||||
|
@ -26,7 +27,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import Api from "common/api";
|
||||||
|
import Axios from "axios";
|
||||||
|
import Notify from "common/notify";
|
||||||
import Event from "pubsub-js";
|
import Event from "pubsub-js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -36,31 +39,75 @@
|
||||||
started: false,
|
started: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
|
subscriptionId: '',
|
||||||
|
fileName: '',
|
||||||
|
source: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
console.log("SUBMIT");
|
// DO NOTHING
|
||||||
},
|
},
|
||||||
startIndexing() {
|
startIndexing() {
|
||||||
|
this.source = Axios.CancelToken.source();
|
||||||
this.started = Date.now();
|
this.started = Date.now();
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.completed = 0;
|
this.completed = 0;
|
||||||
|
this.fileName = '';
|
||||||
this.$alert.info("Indexing photos...");
|
|
||||||
|
|
||||||
const ctx = this;
|
const ctx = this;
|
||||||
|
Notify.blockUI();
|
||||||
|
|
||||||
axios.post('/api/v1/index').then(function () {
|
Api.post('index', {}, { cancelToken: this.source.token }).then(function () {
|
||||||
Event.publish("alert.success", "Indexing complete");
|
Notify.unblockUI();
|
||||||
ctx.busy = false;
|
ctx.busy = false;
|
||||||
ctx.completed = 100;
|
ctx.completed = 100;
|
||||||
}).catch(function () {
|
ctx.fileName = '';
|
||||||
Event.publish("alert.error", "Indexing failed");
|
}).catch(function (e) {
|
||||||
|
Notify.unblockUI();
|
||||||
|
|
||||||
|
if (Axios.isCancel(e)) {
|
||||||
|
// run in background
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify.error("Indexing failed");
|
||||||
|
|
||||||
ctx.busy = false;
|
ctx.busy = false;
|
||||||
ctx.completed = 0;
|
ctx.completed = 0;
|
||||||
|
ctx.fileName = '';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
handleEvent(ev, data) {
|
||||||
|
if(this.source) {
|
||||||
|
this.source.cancel('run in background');
|
||||||
|
this.source = null;
|
||||||
|
Notify.unblockUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = ev.split('.')[1];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'file':
|
||||||
|
this.busy = true;
|
||||||
|
this.completed = 0;
|
||||||
|
this.fileName = data.fileName;
|
||||||
|
break;
|
||||||
|
case 'completed':
|
||||||
|
this.busy = false;
|
||||||
|
this.completed = 100;
|
||||||
|
this.fileName = '';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.subscriptionId = Event.subscribe('index', this.handleEvent);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
Event.unsubscribe(this.subscriptionId);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<v-btn
|
<v-btn
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
color="blue-grey"
|
color="blue-grey"
|
||||||
class="white--text ml-0"
|
class="white--text ml-0 mt-2"
|
||||||
depressed
|
depressed
|
||||||
@click.stop="uploadDialog()"
|
@click.stop="uploadDialog()"
|
||||||
>
|
>
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import Api from "common/api";
|
||||||
import Event from "pubsub-js";
|
import Notify from "common/notify";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'p-tab-upload',
|
name: 'p-tab-upload',
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
console.log("SUBMIT");
|
// DO NOTHING
|
||||||
},
|
},
|
||||||
uploadDialog() {
|
uploadDialog() {
|
||||||
this.$refs.upload.click();
|
this.$refs.upload.click();
|
||||||
|
@ -67,9 +67,8 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$alert.info("Uploading photos...");
|
Notify.info("Uploading photos...");
|
||||||
|
Notify.blockUI();
|
||||||
Event.publish("ajax.start");
|
|
||||||
|
|
||||||
async function performUpload(ctx) {
|
async function performUpload(ctx) {
|
||||||
for (let i = 0; i < ctx.selected.length; i++) {
|
for (let i = 0; i < ctx.selected.length; i++) {
|
||||||
|
@ -80,7 +79,7 @@
|
||||||
|
|
||||||
formData.append('files', file);
|
formData.append('files', file);
|
||||||
|
|
||||||
await axios.post('/api/v1/upload/' + ctx.started,
|
await Api.post('upload/' + ctx.started,
|
||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -90,7 +89,7 @@
|
||||||
).then(function () {
|
).then(function () {
|
||||||
ctx.completed = Math.round((ctx.current / ctx.total) * 100);
|
ctx.completed = Math.round((ctx.current / ctx.total) * 100);
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
Event.publish("alert.error", "Upload failed");
|
Notify.error("Upload failed");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,17 +98,17 @@
|
||||||
this.indexing = true;
|
this.indexing = true;
|
||||||
const ctx = this;
|
const ctx = this;
|
||||||
|
|
||||||
axios.post('/api/v1/import/upload/' + this.started).then(function () {
|
Api.post('import/upload/' + this.started).then(function () {
|
||||||
Event.publish("alert.success", "Upload complete");
|
Notify.unblockUI();
|
||||||
|
Notify.success("Upload complete");
|
||||||
ctx.busy = false;
|
ctx.busy = false;
|
||||||
ctx.indexing = false;
|
ctx.indexing = false;
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
Event.publish("alert.error", "Failure while importing uploaded files");
|
Notify.unblockUI();
|
||||||
|
Notify.error("Failure while importing uploaded files");
|
||||||
ctx.busy = false;
|
ctx.busy = false;
|
||||||
ctx.indexing = false;
|
ctx.indexing = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
Event.publish("ajax.end");
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
56
frontend/src/pages/login.vue
Normal file
56
frontend/src/pages/login.vue
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-page p-page-login">
|
||||||
|
<v-toolbar flat color="blue-grey lighten-4">
|
||||||
|
<v-toolbar-title>Login</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-container class="pt-5">
|
||||||
|
<p class="subheading">
|
||||||
|
<span>Please enter the admin password to proceed...</span>
|
||||||
|
</p>
|
||||||
|
<v-form ref="form" autocomplete="off" class="p-form-login" @submit.prevent="login" dense>
|
||||||
|
<v-text-field
|
||||||
|
label="Password"
|
||||||
|
color="grey"
|
||||||
|
v-model="password"
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
:append-icon="showPassword ? 'visibility' : 'visibility_off'"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
@click:append="showPassword = !showPassword"
|
||||||
|
></v-text-field>
|
||||||
|
<v-btn color="blue-grey"
|
||||||
|
class="white--text ml-0"
|
||||||
|
depressed
|
||||||
|
@click.stop="login">
|
||||||
|
Sign in
|
||||||
|
<v-icon right dark>vpn_key</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'login',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showPassword: false,
|
||||||
|
password: '',
|
||||||
|
nextUrl: this.$route.params.nextUrl ? this.$route.params.nextUrl : "/",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
login() {
|
||||||
|
this.$session.login('admin', this.password).then(
|
||||||
|
() => {
|
||||||
|
this.$router.push(this.nextUrl);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -86,15 +86,15 @@
|
||||||
const photo = this.results[index];
|
const photo = this.results[index];
|
||||||
|
|
||||||
if (photo.PhotoLat && photo.PhotoLong) {
|
if (photo.PhotoLat && photo.PhotoLong) {
|
||||||
this.$router.push({name: 'Places', query: {lat: photo.PhotoLat, long: photo.PhotoLong}});
|
this.$router.push({name: "places", query: {lat: photo.PhotoLat, long: photo.PhotoLong}});
|
||||||
} else if (photo.LocName) {
|
} else if (photo.LocName) {
|
||||||
this.$router.push({name: 'Places', query: {q: photo.LocName}});
|
this.$router.push({name: "places", query: {q: photo.LocName}});
|
||||||
} else if (photo.LocCity) {
|
} else if (photo.LocCity) {
|
||||||
this.$router.push({name: 'Places', query: {q: photo.LocCity}});
|
this.$router.push({name: "places", query: {q: photo.LocCity}});
|
||||||
} else if (photo.LocCountry) {
|
} else if (photo.LocCountry) {
|
||||||
this.$router.push({name: 'Places', query: {q: photo.LocCountry}});
|
this.$router.push({name: "places", query: {q: photo.LocCountry}});
|
||||||
} else {
|
} else {
|
||||||
this.$router.push({name: 'Places', query: {q: photo.CountryName}});
|
this.$router.push({name: "places", query: {q: photo.CountryName}});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openPhoto(index) {
|
openPhoto(index) {
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
this.scrollDisabled = (response.models.length < this.pageSize);
|
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||||
|
|
||||||
if (this.scrollDisabled) {
|
if (this.scrollDisabled) {
|
||||||
this.$alert.info('All ' + this.results.length + ' photos loaded');
|
this.$notify.info('All ' + this.results.length + ' photos loaded');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -182,9 +182,9 @@
|
||||||
this.scrollDisabled = (response.models.length < this.pageSize);
|
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||||
|
|
||||||
if (this.scrollDisabled) {
|
if (this.scrollDisabled) {
|
||||||
this.$alert.info(this.results.length + ' photos found');
|
this.$notify.info(this.results.length + ' photos found');
|
||||||
} else {
|
} else {
|
||||||
this.$alert.info('More than 50 photos found');
|
this.$notify.info('More than 50 photos found');
|
||||||
|
|
||||||
this.$nextTick(() => this.$emit("scrollRefresh"));
|
this.$nextTick(() => this.$emit("scrollRefresh"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,399 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-infinite-scroll="loadMore" infinite-scroll-disabled="loadMoreDisabled" infinite-scroll-distance="10">
|
|
||||||
<v-container fluid>
|
|
||||||
<v-btn @click.stop="dialog= true">Dialog</v-btn>
|
|
||||||
<v-dialog v-model="dialog" dark fullscreen transition="dialog-bottom-transition">
|
|
||||||
<v-card dark>
|
|
||||||
<v-layout row wrap justify-center class="px-4 py-5">
|
|
||||||
<v-flex md8 xs12>
|
|
||||||
<v-card dark flat>
|
|
||||||
<v-img src="/static/img/tagcloud.jpg" aspect-ratio="1" class="mb-5 mx-5"></v-img>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md4 xs12>
|
|
||||||
<v-card dark flat>
|
|
||||||
<v-card-text>
|
|
||||||
<form>
|
|
||||||
<v-text-field
|
|
||||||
v-model="Title"
|
|
||||||
label="Title"
|
|
||||||
placeholder="Tagcloud"
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="artist"
|
|
||||||
label="Artist"
|
|
||||||
placeholder="Unknown"
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="taken"
|
|
||||||
label="Taken at"
|
|
||||||
placeholder="02/02/19 00:02"
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="location"
|
|
||||||
label="Location"
|
|
||||||
placeholder="Berlin"
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="camera"
|
|
||||||
label="Camera"
|
|
||||||
placeholder="Iphone 5S"
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="lense"
|
|
||||||
label="Lense"
|
|
||||||
placeholder="xxx"
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="aperture"
|
|
||||||
label="Aperture"
|
|
||||||
placeholder=""
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="focal"
|
|
||||||
label="Focal Length"
|
|
||||||
placeholder=""
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-text-field
|
|
||||||
v-model="color"
|
|
||||||
label="Color"
|
|
||||||
placeholder="unknown"
|
|
||||||
></v-text-field>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
<v-combobox
|
|
||||||
v-model="model2"
|
|
||||||
:filter="filter"
|
|
||||||
:hide-no-data="!search"
|
|
||||||
:items="items"
|
|
||||||
:search-input.sync="search"
|
|
||||||
hide-selected
|
|
||||||
label="Tags"
|
|
||||||
multiple
|
|
||||||
small-chips
|
|
||||||
>
|
|
||||||
<template v-slot:no-data>
|
|
||||||
<v-list-tile>
|
|
||||||
<span class="subheading">Create</span>
|
|
||||||
<v-chip
|
|
||||||
color= "blue"
|
|
||||||
label
|
|
||||||
small
|
|
||||||
>
|
|
||||||
search
|
|
||||||
</v-chip>
|
|
||||||
</v-list-tile>
|
|
||||||
</template>
|
|
||||||
<template v-slot:selection="{ item, parent, selected }">
|
|
||||||
<v-chip
|
|
||||||
v-if="item === Object(item)"
|
|
||||||
color= "primary"
|
|
||||||
:selected="selected"
|
|
||||||
label
|
|
||||||
small
|
|
||||||
>
|
|
||||||
<span class="pr-2">
|
|
||||||
item text
|
|
||||||
</span>
|
|
||||||
<v-icon
|
|
||||||
small
|
|
||||||
@click="parent.selectItem(item)"
|
|
||||||
>close</v-icon>
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
<template v-slot:item="{ index, item }">
|
|
||||||
<v-list-tile-content>
|
|
||||||
<v-text-field
|
|
||||||
v-if="editing === item"
|
|
||||||
v-model="editing.text"
|
|
||||||
autofocus
|
|
||||||
flat
|
|
||||||
background-color="transparent"
|
|
||||||
hide-details
|
|
||||||
solo
|
|
||||||
@keyup.enter="edit(index, item)"
|
|
||||||
></v-text-field>
|
|
||||||
<v-chip
|
|
||||||
v-else
|
|
||||||
color="red"
|
|
||||||
dark
|
|
||||||
label
|
|
||||||
small
|
|
||||||
>
|
|
||||||
item text
|
|
||||||
</v-chip>
|
|
||||||
</v-list-tile-content>
|
|
||||||
</template>
|
|
||||||
</v-combobox>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Photo from 'model/photo';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'photos',
|
|
||||||
props: {},
|
|
||||||
data() {
|
|
||||||
const query = this.$route.query;
|
|
||||||
const order = query['order'] ? query['order'] : 'newest';
|
|
||||||
const camera = query['camera'] ? parseInt(query['camera']) : 0;
|
|
||||||
const q = query['q'] ? query['q'] : '';
|
|
||||||
const country = query['country'] ? query['country'] : '';
|
|
||||||
const view = query['view'] ? query['view'] : 'tiles';
|
|
||||||
const cameras = [{ID: 0, CameraModel: 'All Cameras'}].concat(this.$config.getValue('cameras'));
|
|
||||||
const countries = [{
|
|
||||||
LocCountryCode: '',
|
|
||||||
LocCountry: 'All Countries'
|
|
||||||
}].concat(this.$config.getValue('countries'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
'snackbarVisible': false,
|
|
||||||
'snackbarText': '',
|
|
||||||
'advandedSearch': false,
|
|
||||||
'window': {
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
'results': [],
|
|
||||||
'query': {
|
|
||||||
view: view,
|
|
||||||
country: country,
|
|
||||||
camera: camera,
|
|
||||||
order: order,
|
|
||||||
q: q,
|
|
||||||
},
|
|
||||||
'options': {
|
|
||||||
'categories': [
|
|
||||||
{value: '', text: 'All Categories'},
|
|
||||||
{value: 'airport', text: 'Airport'},
|
|
||||||
{value: 'amenity', text: 'Amenity'},
|
|
||||||
{value: 'building', text: 'Building'},
|
|
||||||
{value: 'historic', text: 'Historic'},
|
|
||||||
{value: 'shop', text: 'Shop'},
|
|
||||||
{value: 'tourism', text: 'Tourism'},
|
|
||||||
],
|
|
||||||
'views': [
|
|
||||||
{value: 'tiles', text: 'Tiles'},
|
|
||||||
{value: 'details', text: 'Details'},
|
|
||||||
{value: 'list', text: 'List'},
|
|
||||||
],
|
|
||||||
'countries': countries,
|
|
||||||
'cameras': cameras,
|
|
||||||
'sorting': [
|
|
||||||
{value: 'newest', text: 'Newest first'},
|
|
||||||
{value: 'oldest', text: 'Oldest first'},
|
|
||||||
{value: 'imported', text: 'Recently imported'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'listColumns': [
|
|
||||||
{text: 'Title', value: 'PhotoTitle'},
|
|
||||||
{text: 'Taken At', value: 'TakenAt'},
|
|
||||||
{text: 'City', value: 'LocCity'},
|
|
||||||
{text: 'Country', value: 'LocCountry'},
|
|
||||||
{text: 'Camera', value: 'CameraModel'},
|
|
||||||
{text: 'Favorite', value: 'PhotoFavorite'},
|
|
||||||
],
|
|
||||||
'view': view,
|
|
||||||
'loadMoreDisabled': true,
|
|
||||||
'pageSize': 60,
|
|
||||||
'offset': 0,
|
|
||||||
'lastQuery': {},
|
|
||||||
'submitTimeout': false,
|
|
||||||
'selected': [],
|
|
||||||
'dialog' : true,
|
|
||||||
'dialog2': false,
|
|
||||||
'search': null,
|
|
||||||
'activator': null,
|
|
||||||
'attach': null,
|
|
||||||
'colors': ['green', 'purple', 'indigo', 'primary', 'success', 'orange'],
|
|
||||||
'color': '',
|
|
||||||
'editing': null,
|
|
||||||
'index': -1,
|
|
||||||
'items': [
|
|
||||||
{ header: 'Select a tag or create one' },
|
|
||||||
{text: 'Cat', color: 'primary'},
|
|
||||||
{text: 'Sun', color: 'red'},
|
|
||||||
{text: 'Dog', color: 'primary'},
|
|
||||||
{text: 'Holiday', color: 'primary'},
|
|
||||||
{text: 'Tiger', color: 'primary'},
|
|
||||||
{text: 'Soup', color: 'primary'},
|
|
||||||
{text: 'Night', color: 'primary'},
|
|
||||||
{text: 'Table', color: 'primary'},
|
|
||||||
{text: 'Apple', color: 'primary'},
|
|
||||||
{text: 'Frog', color: 'primary'},
|
|
||||||
],
|
|
||||||
'nonce': 1,
|
|
||||||
'menu': false,
|
|
||||||
'model': [
|
|
||||||
|
|
||||||
],
|
|
||||||
'model2': [
|
|
||||||
{text: 'Cat', color: 'primary'},
|
|
||||||
{text: 'Dog', color: 'primary'},
|
|
||||||
{text: 'Holiday', color: 'primary'},
|
|
||||||
{text: 'Tiger', color: 'primary'},
|
|
||||||
{text: 'Soup', color: 'primary'},
|
|
||||||
{text: 'Night', color: 'primary'},
|
|
||||||
{text: 'Table', color: 'primary'},
|
|
||||||
{text: 'Apple', color: 'primary'},
|
|
||||||
{text: 'Frog', color: 'primary'},
|
|
||||||
],
|
|
||||||
'x': 0,
|
|
||||||
'y': 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
window.removeEventListener('resize', this.handleResize)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleResize() {
|
|
||||||
this.window.width = window.innerWidth;
|
|
||||||
this.window.height = window.innerHeight;
|
|
||||||
},
|
|
||||||
clearSelection() {
|
|
||||||
for (let i = 0; i < this.selected.length; i++) {
|
|
||||||
this.selected[i].selected = false;
|
|
||||||
}
|
|
||||||
this.selected = [];
|
|
||||||
this.updateSnackbar();
|
|
||||||
},
|
|
||||||
updateSnackbar(text) {
|
|
||||||
if (!text) text = "";
|
|
||||||
|
|
||||||
this.snackbarText = text;
|
|
||||||
|
|
||||||
this.snackbarVisible = this.snackbarText !== "";
|
|
||||||
},
|
|
||||||
showSnackbar() {
|
|
||||||
this.snackbarVisible = this.snackbarText !== "";
|
|
||||||
},
|
|
||||||
hideSnackbar() {
|
|
||||||
this.snackbarVisible = false;
|
|
||||||
},
|
|
||||||
selectPhoto(photo, ev) {
|
|
||||||
if (photo.selected) {
|
|
||||||
for (let i = 0; i < this.selected.length; i++) {
|
|
||||||
if (this.selected[i].id === photo.id) {
|
|
||||||
this.selected.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
photo.selected = false;
|
|
||||||
} else {
|
|
||||||
this.selected.push(photo);
|
|
||||||
photo.selected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selected.length > 0) {
|
|
||||||
if (this.selected.length === 1) {
|
|
||||||
this.snackbarText = 'One photo selected';
|
|
||||||
} else {
|
|
||||||
this.snackbarText = this.selected.length + ' photos selected';
|
|
||||||
}
|
|
||||||
this.snackbarVisible = true;
|
|
||||||
} else {
|
|
||||||
this.snackbarText = '';
|
|
||||||
this.snackbarVisible = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
likePhoto(photo) {
|
|
||||||
photo.PhotoFavorite = !photo.PhotoFavorite;
|
|
||||||
photo.like(photo.PhotoFavorite);
|
|
||||||
},
|
|
||||||
deletePhoto(photo) {
|
|
||||||
this.$alert.success('Photo deleted');
|
|
||||||
},
|
|
||||||
formChange(event) {
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
clearQuery() {
|
|
||||||
this.query.q = '';
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
openPhoto(index) {
|
|
||||||
this.$viewer.show(this.results, index)
|
|
||||||
},
|
|
||||||
loadMore() {
|
|
||||||
if (this.loadMoreDisabled) return;
|
|
||||||
|
|
||||||
this.loadMoreDisabled = true;
|
|
||||||
|
|
||||||
this.offset += this.pageSize;
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
count: this.pageSize,
|
|
||||||
offset: this.offset,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(params, this.lastQuery);
|
|
||||||
|
|
||||||
Photo.search(params).then(response => {
|
|
||||||
this.results = this.results.concat(response.models);
|
|
||||||
|
|
||||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
|
||||||
|
|
||||||
if (this.loadMoreDisabled) {
|
|
||||||
this.$alert.info('All ' + this.results.length + ' photos loaded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
search() {
|
|
||||||
this.loadMoreDisabled = true;
|
|
||||||
|
|
||||||
// Don't query the same data more than once:197
|
|
||||||
if (JSON.stringify(this.lastQuery) === JSON.stringify(this.query)) return;
|
|
||||||
|
|
||||||
Object.assign(this.lastQuery, this.query);
|
|
||||||
|
|
||||||
this.offset = 0;
|
|
||||||
|
|
||||||
this.$router.replace({query: this.query});
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
count: this.pageSize,
|
|
||||||
offset: this.offset,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(params, this.query);
|
|
||||||
|
|
||||||
Photo.search(params).then(response => {
|
|
||||||
this.results = response.models;
|
|
||||||
|
|
||||||
this.loadMoreDisabled = (response.models.length < this.pageSize);
|
|
||||||
|
|
||||||
if (this.loadMoreDisabled) {
|
|
||||||
this.$alert.info(this.results.length + ' photos found');
|
|
||||||
} else {
|
|
||||||
this.$alert.info('More than 50 photos found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeRouteLeave(to, from, next) {
|
|
||||||
next()
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
this.handleResize();
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -82,14 +82,14 @@
|
||||||
this.search();
|
this.search();
|
||||||
},
|
},
|
||||||
currentPositionError(error) {
|
currentPositionError(error) {
|
||||||
this.$alert.warning(error.message);
|
this.$notify.warning(error.message);
|
||||||
},
|
},
|
||||||
currentPosition() {
|
currentPosition() {
|
||||||
if ("geolocation" in navigator) {
|
if ("geolocation" in navigator) {
|
||||||
this.$alert.success('Finding your position...');
|
this.$notify.success('Finding your position...');
|
||||||
navigator.geolocation.getCurrentPosition(this.currentPositionSuccess, this.currentPositionError);
|
navigator.geolocation.getCurrentPosition(this.currentPositionSuccess, this.currentPositionError);
|
||||||
} else {
|
} else {
|
||||||
this.$alert.warning('Geolocation is not available');
|
this.$notify.warning('Geolocation is not available');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formChange() {
|
formChange() {
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (photos.length === 0) {
|
if (photos.length === 0) {
|
||||||
this.$alert.warning('No locations found');
|
this.$notify.warning('No locations found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,13 +169,13 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (photos.length > 100) {
|
if (photos.length > 100) {
|
||||||
this.$alert.info('More than 100 photos found');
|
this.$notify.info('More than 100 photos found');
|
||||||
} else {
|
} else {
|
||||||
this.$alert.info(photos.length + ' photos found');
|
this.$notify.info(photos.length + ' photos found');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateQuery() {
|
updateQuery() {
|
||||||
this.$router.replace({query: this.query});
|
this.$router.replace({query: this.query}).catch(err => {});
|
||||||
|
|
||||||
if(this.query.lat && this.query.long) {
|
if(this.query.lat && this.query.long) {
|
||||||
this.position = L.latLng(this.query.lat, this.query.long);
|
this.position = L.latLng(this.query.lat, this.query.long);
|
||||||
|
@ -194,7 +194,7 @@
|
||||||
|
|
||||||
this.updateQuery();
|
this.updateQuery();
|
||||||
|
|
||||||
this.$router.replace({query: this.query});
|
this.$router.replace({query: this.query}).catch(err => {});
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
count: this.pageSize,
|
count: this.pageSize,
|
||||||
|
@ -206,7 +206,7 @@
|
||||||
|
|
||||||
Photo.search(params).then(response => {
|
Photo.search(params).then(response => {
|
||||||
if (!response.models.length) {
|
if (!response.models.length) {
|
||||||
this.$alert.warning('No photos found');
|
this.$notify.warning('No photos found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="p-page p-page-settings">
|
||||||
<v-toolbar flat color="blue-grey lighten-4">
|
<v-tabs
|
||||||
<v-toolbar-title>Not implemented yet</v-toolbar-title>
|
v-model="active"
|
||||||
|
flat
|
||||||
<v-spacer></v-spacer>
|
grow
|
||||||
</v-toolbar>
|
color="blue-grey lighten-4"
|
||||||
|
slider-color="blue-grey darken-1"
|
||||||
<v-container>
|
height="64"
|
||||||
<p>
|
>
|
||||||
Issues labeled <a href="https://github.com/photoprism/photoprism/labels/help%20wanted">help wanted</a> /
|
<v-tab id="tab-upload">
|
||||||
<a href="https://github.com/photoprism/photoprism/labels/easy">easy</a> can be good (first)
|
General
|
||||||
contributions.
|
</v-tab>
|
||||||
Our <a href="https://github.com/photoprism/photoprism/wiki">Developer Guide</a> contains all information
|
<v-tab-item>
|
||||||
necessary to get you started.
|
<p-tab-general></p-tab-general>
|
||||||
</p>
|
</v-tab-item>
|
||||||
</v-container>
|
</v-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import tabGeneral from "pages/settings/general.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'todo',
|
name: 'p-page-settings',
|
||||||
data() {
|
components: {
|
||||||
return {};
|
'p-tab-general': tabGeneral,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
readonly: this.$config.getValue("readonly"),
|
||||||
|
active: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
},
|
},
|
||||||
methods: {}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
68
frontend/src/pages/settings/general.vue
Normal file
68
frontend/src/pages/settings/general.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-tab p-tab-general">
|
||||||
|
<v-container fluid>
|
||||||
|
<v-form ref="form" class="p-form-settings" lazy-validation @submit.prevent="save" dense>
|
||||||
|
<v-layout wrap align-center>
|
||||||
|
<v-flex xs12 sm6 class="pr-3">
|
||||||
|
<v-select
|
||||||
|
:items="languages"
|
||||||
|
label="Language"
|
||||||
|
color="blue-grey"
|
||||||
|
v-model="settings.language"
|
||||||
|
flat
|
||||||
|
></v-select>
|
||||||
|
</v-flex>
|
||||||
|
|
||||||
|
<v-flex xs12 sm6 class="pr-3">
|
||||||
|
<v-select
|
||||||
|
:items="themes"
|
||||||
|
label="Theme"
|
||||||
|
color="blue-grey"
|
||||||
|
v-model="settings.theme"
|
||||||
|
flat
|
||||||
|
></v-select>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
|
||||||
|
<v-btn color="blue-grey"
|
||||||
|
class="white--text ml-0 mt-2"
|
||||||
|
depressed
|
||||||
|
@click.stop="save">
|
||||||
|
Save
|
||||||
|
<v-icon right dark>save</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Settings from "model/settings";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'p-tab-general',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
readonly: this.$config.getValue("readonly"),
|
||||||
|
active: 0,
|
||||||
|
settings: new Settings(this.$config.values.settings),
|
||||||
|
list: {},
|
||||||
|
themes: [{text: "Dark", value: "dark"}, {text: "Light", value: "light"}],
|
||||||
|
languages: [{text: "English", value: "en"}],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.settings.load().then((r) => { this.list = r.getValues(); });
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
this.settings.save().then(() => {
|
||||||
|
this.$notify.info("Settings saved");
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -2,7 +2,7 @@
|
||||||
<div id="photoprism">
|
<div id="photoprism">
|
||||||
<p-loading-bar height="4"></p-loading-bar>
|
<p-loading-bar height="4"></p-loading-bar>
|
||||||
|
|
||||||
<p-alert></p-alert>
|
<p-notify></p-notify>
|
||||||
|
|
||||||
<v-app>
|
<v-app>
|
||||||
<p-navigation></p-navigation>
|
<p-navigation></p-navigation>
|
||||||
|
@ -22,14 +22,6 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'photoprism',
|
name: 'photoprism',
|
||||||
computed: {},
|
computed: {},
|
||||||
methods: {
|
methods: {},
|
||||||
login() {
|
|
||||||
// this.$refs.loginDialog.open();
|
|
||||||
},
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.$session.logout();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,80 +7,87 @@ import People from "pages/people.vue";
|
||||||
import Library from "pages/library.vue";
|
import Library from "pages/library.vue";
|
||||||
import Share from "pages/share.vue";
|
import Share from "pages/share.vue";
|
||||||
import Settings from "pages/settings.vue";
|
import Settings from "pages/settings.vue";
|
||||||
|
import Login from "pages/login.vue";
|
||||||
import Todo from "pages/todo.vue";
|
import Todo from "pages/todo.vue";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
name: "Home",
|
name: "home",
|
||||||
path: "/",
|
path: "/",
|
||||||
redirect: "/photos",
|
redirect: "/photos",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Photos",
|
name: "login",
|
||||||
|
path: "/login",
|
||||||
|
component: Login,
|
||||||
|
meta: {area: "Login"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "photos",
|
||||||
path: "/photos",
|
path: "/photos",
|
||||||
component: Photos,
|
component: Photos,
|
||||||
meta: {area: "Photos"},
|
meta: {area: "Photos"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Albums",
|
name: "albums",
|
||||||
path: "/albums",
|
path: "/albums",
|
||||||
component: Albums,
|
component: Albums,
|
||||||
meta: {area: "Albums"},
|
meta: {area: "Albums"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Favorites",
|
name: "favorites",
|
||||||
path: "/favorites",
|
path: "/favorites",
|
||||||
component: Photos,
|
component: Photos,
|
||||||
meta: {area: "Favorites"},
|
meta: {area: "Favorites"},
|
||||||
props: {staticFilter: {favorites: true}},
|
props: {staticFilter: {favorites: true}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Places",
|
name: "places",
|
||||||
path: "/places",
|
path: "/places",
|
||||||
component: Places,
|
component: Places,
|
||||||
meta: {area: "Places"},
|
meta: {area: "Places"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Labels",
|
name: "labels",
|
||||||
path: "/labels",
|
path: "/labels",
|
||||||
component: Labels,
|
component: Labels,
|
||||||
meta: {area: "Labels"},
|
meta: {area: "Labels"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Events",
|
name: "events",
|
||||||
path: "/events",
|
path: "/events",
|
||||||
component: Events,
|
component: Events,
|
||||||
meta: {area: "Events"},
|
meta: {area: "Events"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "People",
|
name: "people",
|
||||||
path: "/people",
|
path: "/people",
|
||||||
component: People,
|
component: People,
|
||||||
meta: {area: "People"},
|
meta: {area: "People"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Filters",
|
name: "filters",
|
||||||
path: "/filters",
|
path: "/filters",
|
||||||
component: Todo,
|
component: Todo,
|
||||||
meta: {area: "Filters"},
|
meta: {area: "Filters"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Library",
|
name: "library",
|
||||||
path: "/library",
|
path: "/library",
|
||||||
component: Library,
|
component: Library,
|
||||||
meta: {area: "Library"},
|
meta: {area: "Library", auth: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Share",
|
name: "share",
|
||||||
path: "/share",
|
path: "/share",
|
||||||
component: Share,
|
component: Share,
|
||||||
meta: {area: "Share"},
|
meta: {area: "Share", auth: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "settings",
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
component: Settings,
|
component: Settings,
|
||||||
meta: {area: "Settings"},
|
meta: {area: "Settings", auth: true},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "*", redirect: "/photos",
|
path: "*", redirect: "/photos",
|
||||||
|
|
5
frontend/src/session.js
Normal file
5
frontend/src/session.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Session from "common/session";
|
||||||
|
|
||||||
|
const session = new Session(window.localStorage);
|
||||||
|
|
||||||
|
export default session;
|
|
@ -20,8 +20,4 @@ test('Navigate', async t => {
|
||||||
await t
|
await t
|
||||||
.click('a[href="/labels"]')
|
.click('a[href="/labels"]')
|
||||||
.expect(Selector('div.p-page-labels').exists, {timeout: 5000}).ok();
|
.expect(Selector('div.p-page-labels').exists, {timeout: 5000}).ok();
|
||||||
await page.openNav();
|
|
||||||
await t
|
|
||||||
.click('a[href="/library"]')
|
|
||||||
.expect(Selector('div.p-tab-upload').exists, {timeout: 5000}).ok();
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -53,18 +53,4 @@ describe("common/config", () => {
|
||||||
const result = config.getValue("city");
|
const result = config.getValue("city");
|
||||||
assert.equal(result, "Berlin");
|
assert.equal(result, "Berlin");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should pull from server", async() => {
|
|
||||||
mock.onGet("config").reply(200, {fromServer: "yes"});
|
|
||||||
const storage = window.localStorage;
|
|
||||||
const values = {name: "testConfig", year: "2300"};
|
|
||||||
|
|
||||||
const config = new Config(storage, values);
|
|
||||||
const result = config.getValues();
|
|
||||||
assert.equal(result.name, "testConfig");
|
|
||||||
assert.equal(config.values.fromServer, undefined);
|
|
||||||
await config.pullFromServer();
|
|
||||||
assert.equal(config.values.fromServer, "yes");
|
|
||||||
mock.reset();
|
|
||||||
});
|
|
||||||
});
|
});
|
|
@ -1,31 +1,31 @@
|
||||||
import Alert from "common/alert";
|
import Notify from "common/notify";
|
||||||
let sinon = require("sinon");
|
let sinon = require("sinon");
|
||||||
|
|
||||||
describe("common/alert", () => {
|
describe("common/alert", () => {
|
||||||
it("should call alert.info", () => {
|
it("should call alert.info", () => {
|
||||||
let spy = sinon.spy(Alert, "info");
|
let spy = sinon.spy(Notify, "info");
|
||||||
Alert.info("message");
|
Notify.info("message");
|
||||||
sinon.assert.calledOnce(spy);
|
sinon.assert.calledOnce(spy);
|
||||||
spy.resetHistory();
|
spy.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call alert.warning", () => {
|
it("should call alert.warning", () => {
|
||||||
let spy = sinon.spy(Alert, "warning");
|
let spy = sinon.spy(Notify, "warning");
|
||||||
Alert.warning("message");
|
Notify.warning("message");
|
||||||
sinon.assert.calledOnce(spy);
|
sinon.assert.calledOnce(spy);
|
||||||
spy.resetHistory();
|
spy.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call alert.error", () => {
|
it("should call alert.error", () => {
|
||||||
let spy = sinon.spy(Alert, "error");
|
let spy = sinon.spy(Notify, "error");
|
||||||
Alert.error("message");
|
Notify.error("message");
|
||||||
sinon.assert.calledOnce(spy);
|
sinon.assert.calledOnce(spy);
|
||||||
spy.resetHistory();
|
spy.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call alert.success", () => {
|
it("should call alert.success", () => {
|
||||||
let spy = sinon.spy(Alert, "success");
|
let spy = sinon.spy(Notify, "success");
|
||||||
Alert.success("message");
|
Notify.success("message");
|
||||||
sinon.assert.calledOnce(spy);
|
sinon.assert.calledOnce(spy);
|
||||||
spy.resetHistory();
|
spy.resetHistory();
|
||||||
});
|
});
|
|
@ -35,15 +35,15 @@ describe('common/session', () => {
|
||||||
it('should set, get and delete user', () => {
|
it('should set, get and delete user', () => {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
assert.equal(session.user.ID, undefined);
|
assert.equal(session.user, null);
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
session.setUser(user);
|
session.setUser(user);
|
||||||
assert.equal(session.user.userFirstName, "Max");
|
assert.equal(session.user.FirstName, "Max");
|
||||||
assert.equal(session.user.userRole, "admin");
|
assert.equal(session.user.Role, "admin");
|
||||||
const result = session.getUser();
|
const result = session.getUser();
|
||||||
assert.equal(result.ID, 5);
|
assert.equal(result.ID, 5);
|
||||||
assert.equal(result.userEmail, "test@test.com");
|
assert.equal(result.Email, "test@test.com");
|
||||||
session.deleteUser();
|
session.deleteUser();
|
||||||
assert.equal(session.user, null);
|
assert.equal(session.user, null);
|
||||||
});
|
});
|
||||||
|
@ -51,12 +51,12 @@ describe('common/session', () => {
|
||||||
it('should get user email', () => {
|
it('should get user email', () => {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
session.setUser(user);
|
session.setUser(user);
|
||||||
const result = session.getEmail();
|
const result = session.getEmail();
|
||||||
assert.equal(result, "test@test.com");
|
assert.equal(result, "test@test.com");
|
||||||
const values2 = { userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values2 = { FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user2 = new User(values2);
|
const user2 = new User(values2);
|
||||||
session.setUser(user2);
|
session.setUser(user2);
|
||||||
const result2 = session.getEmail();
|
const result2 = session.getEmail();
|
||||||
|
@ -67,12 +67,12 @@ describe('common/session', () => {
|
||||||
it('should get user firstname', () => {
|
it('should get user firstname', () => {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
session.setUser(user);
|
session.setUser(user);
|
||||||
const result = session.getFirstName();
|
const result = session.getFirstName();
|
||||||
assert.equal(result, "Max");
|
assert.equal(result, "Max");
|
||||||
const values2 = { userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values2 = { FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user2 = new User(values2);
|
const user2 = new User(values2);
|
||||||
session.setUser(user2);
|
session.setUser(user2);
|
||||||
const result2 = session.getFirstName();
|
const result2 = session.getFirstName();
|
||||||
|
@ -83,12 +83,12 @@ describe('common/session', () => {
|
||||||
it('should get user full name', () => {
|
it('should get user full name', () => {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
session.setUser(user);
|
session.setUser(user);
|
||||||
const result = session.getFullName();
|
const result = session.getFullName();
|
||||||
assert.equal(result, "Max Last");
|
assert.equal(result, "Max Last");
|
||||||
const values2 = { userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values2 = { FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user2 = new User(values2);
|
const user2 = new User(values2);
|
||||||
session.setUser(user2);
|
session.setUser(user2);
|
||||||
const result2 = session.getFullName();
|
const result2 = session.getFullName();
|
||||||
|
@ -99,7 +99,7 @@ describe('common/session', () => {
|
||||||
it('should test whether user is set', () => {
|
it('should test whether user is set', () => {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
session.setUser(user);
|
session.setUser(user);
|
||||||
const result = session.isUser();
|
const result = session.isUser();
|
||||||
|
@ -110,7 +110,7 @@ describe('common/session', () => {
|
||||||
it('should test whether user is admin', () => {
|
it('should test whether user is admin', () => {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
session.setUser(user);
|
session.setUser(user);
|
||||||
const result = session.isAdmin();
|
const result = session.isAdmin();
|
||||||
|
@ -121,7 +121,7 @@ describe('common/session', () => {
|
||||||
it('should test whether user is anonymous', () => {
|
it('should test whether user is anonymous', () => {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
session.setUser(user);
|
session.setUser(user);
|
||||||
const result = session.isAnonymous();
|
const result = session.isAnonymous();
|
||||||
|
@ -131,7 +131,7 @@ describe('common/session', () => {
|
||||||
|
|
||||||
it('should test login and logout', async() => {
|
it('should test login and logout', async() => {
|
||||||
mock
|
mock
|
||||||
.onPost("session").reply(200, {token: "8877", user: {email: "test@test.com", password: "passwd"}})
|
.onPost("session").reply(200, {token: "8877", user: {ID: 1, Email: "test@test.com"}})
|
||||||
.onDelete("session/8877").reply(200);
|
.onDelete("session/8877").reply(200);
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const session = new Session(storage);
|
const session = new Session(storage);
|
||||||
|
@ -139,7 +139,7 @@ describe('common/session', () => {
|
||||||
assert.equal(session.storage.user, undefined);
|
assert.equal(session.storage.user, undefined);
|
||||||
await session.login("test@test.com", "passwd");
|
await session.login("test@test.com", "passwd");
|
||||||
assert.equal(session.session_token, 8877);
|
assert.equal(session.session_token, 8877);
|
||||||
assert.equal(session.storage.user, "{\"email\":\"test@test.com\",\"password\":\"passwd\"}");
|
assert.equal(session.storage.user, '{"ID":1,"Email":"test@test.com"}');
|
||||||
await session.logout();
|
await session.logout();
|
||||||
assert.equal(session.session_token, null);
|
assert.equal(session.session_token, null);
|
||||||
mock.reset();
|
mock.reset();
|
||||||
|
|
|
@ -9,14 +9,14 @@ describe("model/user", () => {
|
||||||
const mock = new MockAdapter(Api);
|
const mock = new MockAdapter(Api);
|
||||||
|
|
||||||
it("should get entity name", () => {
|
it("should get entity name", () => {
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = user.getEntityName();
|
const result = user.getEntityName();
|
||||||
assert.equal(result, "Max Last");
|
assert.equal(result, "Max Last");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get id", () => {
|
it("should get id", () => {
|
||||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = user.getId();
|
const result = user.getId();
|
||||||
assert.equal(result, 5);
|
assert.equal(result, 5);
|
||||||
|
@ -34,7 +34,7 @@ describe("model/user", () => {
|
||||||
|
|
||||||
it("should get register form", async() => {
|
it("should get register form", async() => {
|
||||||
mock.onAny("users/52/register").reply(200, "registerForm");
|
mock.onAny("users/52/register").reply(200, "registerForm");
|
||||||
const values = {ID: 52, userFirstName: "Max"};
|
const values = {ID: 52, FirstName: "Max"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = await user.getRegisterForm();
|
const result = await user.getRegisterForm();
|
||||||
assert.equal(result.definition, "registerForm");
|
assert.equal(result.definition, "registerForm");
|
||||||
|
@ -43,7 +43,7 @@ describe("model/user", () => {
|
||||||
|
|
||||||
it("should get profile form", async() => {
|
it("should get profile form", async() => {
|
||||||
mock.onAny("users/53/profile").reply(200, "profileForm");
|
mock.onAny("users/53/profile").reply(200, "profileForm");
|
||||||
const values = {ID: 53, userFirstName: "Max"};
|
const values = {ID: 53, FirstName: "Max"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = await user.getProfileForm();
|
const result = await user.getProfileForm();
|
||||||
assert.equal(result.definition, "profileForm");
|
assert.equal(result.definition, "profileForm");
|
||||||
|
@ -52,20 +52,20 @@ describe("model/user", () => {
|
||||||
|
|
||||||
it("should get change password", async() => {
|
it("should get change password", async() => {
|
||||||
mock.onPut("users/54/password").reply(200, {password: "old", new_password: "new"});
|
mock.onPut("users/54/password").reply(200, {password: "old", new_password: "new"});
|
||||||
const values = {ID: 54, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 54, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
const result = await user.changePassword("old", "new");
|
const result = await user.changePassword("old", "new");
|
||||||
assert.equal(result.new_password, "new");
|
assert.equal(result.new_password, "new");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save profile", async() => {
|
it("should save profile", async() => {
|
||||||
mock.onPost("users/55/profile").reply(200, {userFirstName: "MaxNew", userLastName: "LastNew"});
|
mock.onPost("users/55/profile").reply(200, {FirstName: "MaxNew", LastName: "LastNew"});
|
||||||
const values = {ID: 55, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
const values = {ID: 55, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||||
const user = new User(values);
|
const user = new User(values);
|
||||||
assert.equal(user.userFirstName, "Max");
|
assert.equal(user.FirstName, "Max");
|
||||||
assert.equal(user.userLastName, "Last");
|
assert.equal(user.LastName, "Last");
|
||||||
await user.saveProfile();
|
await user.saveProfile();
|
||||||
assert.equal(user.userFirstName, "MaxNew");
|
assert.equal(user.FirstName, "MaxNew");
|
||||||
assert.equal(user.userLastName, "LastNew");
|
assert.equal(user.LastName, "LastNew");
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -199,7 +199,7 @@ const config = {
|
||||||
// No sourcemap for production
|
// No sourcemap for production
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
const devToolPlugin = new webpack.SourceMapDevToolPlugin({
|
const devToolPlugin = new webpack.SourceMapDevToolPlugin({
|
||||||
filename: "[name].map",
|
filename: "[file].map",
|
||||||
});
|
});
|
||||||
|
|
||||||
config.plugins.push(devToolPlugin);
|
config.plugins.push(devToolPlugin);
|
||||||
|
|
16
go.mod
16
go.mod
|
@ -13,11 +13,14 @@ require (
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f
|
github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f
|
||||||
github.com/dsoprea/go-logging v0.0.0-20190409182557-13b4fff49234 // indirect
|
github.com/dsoprea/go-logging v0.0.0-20190409182557-13b4fff49234 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.3.0
|
github.com/gin-gonic/gin v1.3.0
|
||||||
github.com/go-errors/errors v1.0.1 // indirect
|
github.com/go-errors/errors v1.0.1 // indirect
|
||||||
github.com/golang/geo v0.0.0-20190507233405-a0e886e97a51 // indirect
|
github.com/golang/geo v0.0.0-20190507233405-a0e886e97a51 // indirect
|
||||||
github.com/gorilla/websocket v1.4.0 // indirect
|
github.com/golang/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
|
github.com/google/go-cmp v0.3.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.1
|
||||||
github.com/gosimple/slug v1.5.0
|
github.com/gosimple/slug v1.5.0
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 // indirect
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.5.1 // indirect
|
github.com/grpc-ecosystem/grpc-gateway v1.5.1 // indirect
|
||||||
|
@ -25,6 +28,7 @@ require (
|
||||||
github.com/json-iterator/go v1.1.5 // indirect
|
github.com/json-iterator/go v1.1.5 // indirect
|
||||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
|
github.com/leandro-lugaresi/hub v1.1.0
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2
|
github.com/lucasb-eyer/go-colorful v1.0.2
|
||||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
github.com/mattn/go-isatty v0.0.4 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
@ -32,6 +36,7 @@ require (
|
||||||
github.com/montanaflynn/stats v0.0.0-20181214052348-945b007cb92f // indirect
|
github.com/montanaflynn/stats v0.0.0-20181214052348-945b007cb92f // indirect
|
||||||
github.com/myesui/uuid v1.0.0 // indirect
|
github.com/myesui/uuid v1.0.0 // indirect
|
||||||
github.com/opentracing/opentracing-go v1.0.2
|
github.com/opentracing/opentracing-go v1.0.2
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pingcap/errors v0.11.1
|
github.com/pingcap/errors v0.11.1
|
||||||
github.com/pingcap/goleveldb v0.0.0-20171020122428-b9ff6c35079e // indirect
|
github.com/pingcap/goleveldb v0.0.0-20171020122428-b9ff6c35079e // indirect
|
||||||
github.com/pingcap/parser v0.0.0-20190529073816-0550d84c65ad
|
github.com/pingcap/parser v0.0.0-20190529073816-0550d84c65ad
|
||||||
|
@ -55,16 +60,19 @@ require (
|
||||||
github.com/uber-go/atomic v1.3.2 // indirect
|
github.com/uber-go/atomic v1.3.2 // indirect
|
||||||
github.com/uber/jaeger-client-go v2.15.0+incompatible // indirect
|
github.com/uber/jaeger-client-go v2.15.0+incompatible // indirect
|
||||||
github.com/uber/jaeger-lib v1.5.0 // indirect
|
github.com/uber/jaeger-lib v1.5.0 // indirect
|
||||||
|
github.com/ugorji/go v1.1.7 // indirect
|
||||||
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f // indirect
|
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f // indirect
|
||||||
github.com/urfave/cli v1.20.0
|
github.com/urfave/cli v1.20.0
|
||||||
|
go.uber.org/atomic v1.4.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d // indirect
|
golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d // indirect
|
||||||
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect
|
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect
|
||||||
golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 // indirect
|
|
||||||
golang.org/x/text v0.3.1 // indirect
|
golang.org/x/text v0.3.1 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
|
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
|
||||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
|
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
|
||||||
|
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
|
||||||
gopkg.in/ugjka/go-tz.v2 v2.0.8
|
gopkg.in/ugjka/go-tz.v2 v2.0.8
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
gopkg.in/yaml.v2 v2.2.2
|
||||||
)
|
)
|
||||||
|
|
45
go.sum
45
go.sum
|
@ -59,11 +59,6 @@ github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJ
|
||||||
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||||
github.com/djherbis/times v1.1.0 h1:NFhBDODme0XNX+/5ETW9qL6v3Ty57psiXIQBrzzg44E=
|
github.com/djherbis/times v1.1.0 h1:NFhBDODme0XNX+/5ETW9qL6v3Ty57psiXIQBrzzg44E=
|
||||||
github.com/djherbis/times v1.1.0/go.mod h1:CGMZlo255K5r4Yw0b9RRfFQpM2y7uOmxg4jm9HsaVf8=
|
github.com/djherbis/times v1.1.0/go.mod h1:CGMZlo255K5r4Yw0b9RRfFQpM2y7uOmxg4jm9HsaVf8=
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190527162249-12b8993a44a567ccf02080c8e9cd0885c92a8e7a/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c=
|
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190527162249-17eaca42337c h1:XIvtrgkakwGk0WzvHVKpQQxCR3MTZEqgOpTymwg+stY=
|
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190527162249-17eaca42337c/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c=
|
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190624162249-12b8993a44a567ccf02080c8e9cd0885c92a8e7a h1:ZBQj1uonO+8tjOSroKkZofHGrSAG0l+hMIOiDw5Zxiw=
|
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190624162249-12b8993a44a567ccf02080c8e9cd0885c92a8e7a/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c=
|
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f h1:vqfYiZ+xF0xJvl9SZ1kovmMgKjaGZIz/Hn8JDQdyd9A=
|
github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f h1:vqfYiZ+xF0xJvl9SZ1kovmMgKjaGZIz/Hn8JDQdyd9A=
|
||||||
github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c=
|
github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c=
|
||||||
github.com/dsoprea/go-logging v0.0.0-20190409182557-13b4fff49234 h1:WPgjL7Z/Cn2a0kEVtwCqh8BqYjmY8RPGvor/jQZVmzk=
|
github.com/dsoprea/go-logging v0.0.0-20190409182557-13b4fff49234 h1:WPgjL7Z/Cn2a0kEVtwCqh8BqYjmY8RPGvor/jQZVmzk=
|
||||||
|
@ -84,8 +79,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
|
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
|
||||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||||
|
@ -111,13 +106,19 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
||||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/btree v0.0.0-20161217183710-316fb6d3f031/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20161217183710-316fb6d3f031/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
|
||||||
|
@ -128,8 +129,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
|
||||||
github.com/gorilla/mux v0.0.0-20170228224354-599cba5e7b61/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v0.0.0-20170228224354-599cba5e7b61/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gosimple/slug v1.5.0 h1:AIIjgCjHcLpX8LzM2NpG4QGW9kUfqv0OLiFRfPv/H3E=
|
github.com/gosimple/slug v1.5.0 h1:AIIjgCjHcLpX8LzM2NpG4QGW9kUfqv0OLiFRfPv/H3E=
|
||||||
github.com/gosimple/slug v1.5.0/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
|
github.com/gosimple/slug v1.5.0/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20171020063731-82921fcf811d/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20171020063731-82921fcf811d/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
@ -166,6 +167,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leandro-lugaresi/hub v1.1.0 h1:yHYA0WsMYaJd+I6J24nYlCP2CFD4RTnhaHCRmKjv3q4=
|
||||||
|
github.com/leandro-lugaresi/hub v1.1.0/go.mod h1:IVKrfZTYfU1SbWCGQMHNGYdW4j1Pl7Cg8gr6sSeT/84=
|
||||||
github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
|
github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
|
||||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||||
|
@ -204,6 +207,8 @@ github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKw
|
||||||
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
|
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
|
||||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg=
|
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg=
|
||||||
|
@ -256,12 +261,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446 h1:/NRJ5vAYoqz+7sG51ubIDHXeWO8DlTSrToPu6q11ziA=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7 h1:FUL3b97ZY2EPqg2NbXKuMHs5pXJB9hjj1fDHnF2vl28=
|
github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7 h1:FUL3b97ZY2EPqg2NbXKuMHs5pXJB9hjj1fDHnF2vl28=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20190806203942-babf20351dd7e3ac320adedbbe5eb311aec8763c h1:eED6LswgZ3TfAl9fb+L2TfdSlXpYdg21iWZMdHuoSks=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20190806203942-babf20351dd7e3ac320adedbbe5eb311aec8763c/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk=
|
github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk=
|
||||||
|
@ -284,8 +285,6 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/tensorflow/tensorflow v1.13.1 h1:ygn0+ztXusm6RGVP4Od5IF+8h5sAgD5qbeTvqYyMnjo=
|
|
||||||
github.com/tensorflow/tensorflow v1.13.1/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
|
|
||||||
github.com/tensorflow/tensorflow v1.14.0 h1:g0W2+f/RybcvmrTjPLTwXkfr/BsDGUd8FKT6ZzojOMo=
|
github.com/tensorflow/tensorflow v1.14.0 h1:g0W2+f/RybcvmrTjPLTwXkfr/BsDGUd8FKT6ZzojOMo=
|
||||||
github.com/tensorflow/tensorflow v1.14.0/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
|
github.com/tensorflow/tensorflow v1.14.0/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc=
|
||||||
|
@ -305,6 +304,10 @@ github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo
|
||||||
github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
|
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
|
||||||
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/unrolled/render v0.0.0-20171102162132-65450fb6b2d3/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
|
github.com/unrolled/render v0.0.0-20171102162132-65450fb6b2d3/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
|
||||||
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f h1:+feYJlxPM00jEkdybexHiwIIOVuClwTEbh1WLiNr0mk=
|
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f h1:+feYJlxPM00jEkdybexHiwIIOVuClwTEbh1WLiNr0mk=
|
||||||
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
|
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
|
||||||
|
@ -316,6 +319,8 @@ github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Y
|
||||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
||||||
|
@ -350,8 +355,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTm
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -359,8 +362,8 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||||
golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@ -369,6 +372,8 @@ golang.org/x/text v0.3.1/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac h1:0Nb35Izc6T6Yz1iGmRc4cg14cxRaFjbjD4hWFI6JNJ8=
|
golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac h1:0Nb35Izc6T6Yz1iGmRc4cg14cxRaFjbjD4hWFI6JNJ8=
|
||||||
|
@ -408,6 +413,8 @@ gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY
|
||||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
|
gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M=
|
||||||
|
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/ugjka/go-tz.v2 v2.0.8 h1:EmQ1tY6aa9upe1EDqyPdyHgM9STimr2fw7+b/CuMQ94=
|
gopkg.in/ugjka/go-tz.v2 v2.0.8 h1:EmQ1tY6aa9upe1EDqyPdyHgM9STimr2fw7+b/CuMQ94=
|
||||||
|
|
|
@ -13,8 +13,6 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/forms"
|
"github.com/photoprism/photoprism/internal/forms"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /api/v1/albums
|
// GET /api/v1/albums
|
||||||
|
@ -36,8 +34,8 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
c.Header("X-Result-Count", strconv.Itoa(form.Count))
|
||||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
c.Header("X-Result-Offset", strconv.Itoa(form.Offset))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
})
|
})
|
||||||
|
@ -50,6 +48,11 @@ type CreateAlbumParams struct {
|
||||||
// POST /api/v1/albums
|
// POST /api/v1/albums
|
||||||
func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/albums", func(c *gin.Context) {
|
router.POST("/albums", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var params CreateAlbumParams
|
var params CreateAlbumParams
|
||||||
|
|
||||||
if err := c.BindJSON(¶ms); err != nil {
|
if err := c.BindJSON(¶ms); err != nil {
|
||||||
|
@ -75,6 +78,11 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// uuid: string Album UUID
|
// uuid: string Album UUID
|
||||||
func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/albums/:uuid/like", func(c *gin.Context) {
|
router.POST("/albums/:uuid/like", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
||||||
|
|
||||||
album, err := search.FindAlbumByUUID(c.Param("uuid"))
|
album, err := search.FindAlbumByUUID(c.Param("uuid"))
|
||||||
|
@ -97,6 +105,11 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// uuid: string Album UUID
|
// uuid: string Album UUID
|
||||||
func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.DELETE("/albums/:uuid/like", func(c *gin.Context) {
|
router.DELETE("/albums/:uuid/like", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
||||||
|
|
||||||
album, err := search.FindAlbumByUUID(c.Param("uuid"))
|
album, err := search.FindAlbumByUUID(c.Param("uuid"))
|
||||||
|
|
|
@ -8,24 +8,24 @@ import (
|
||||||
|
|
||||||
func TestGetAlbums(t *testing.T) {
|
func TestGetAlbums(t *testing.T) {
|
||||||
t.Run("successful request", func(t *testing.T) {
|
t.Run("successful request", func(t *testing.T) {
|
||||||
app, router, ctx := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
GetAlbums(router, ctx)
|
GetAlbums(router, conf)
|
||||||
result := PerformRequest(app, "GET", "/api/v1/albums?count=10")
|
result := PerformRequest(app, "GET", "/api/v1/albums?count=10")
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, result.Code)
|
assert.Equal(t, http.StatusOK, result.Code)
|
||||||
})
|
})
|
||||||
t.Run("invalid request", func(t *testing.T) {
|
t.Run("invalid request", func(t *testing.T) {
|
||||||
app, router, ctx := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
GetAlbums(router, ctx)
|
GetAlbums(router, conf)
|
||||||
result := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
result := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
||||||
t.Log(result.Body)
|
t.Log(result.Body)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusBadRequest, result.Code)
|
assert.Equal(t, http.StatusBadRequest, result.Code)
|
||||||
})
|
})
|
||||||
t.Run("invalid request", func(t *testing.T) {
|
t.Run("invalid request", func(t *testing.T) {
|
||||||
app, router, ctx := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
t.Log(router)
|
t.Log(router)
|
||||||
t.Log(ctx)
|
t.Log(conf)
|
||||||
result := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
result := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
||||||
t.Log(result.Body)
|
t.Log(result.Body)
|
||||||
|
|
||||||
|
@ -47,9 +47,9 @@ func TestLikeAlbum(t *testing.T) {
|
||||||
|
|
||||||
func TestDislikeAlbum(t *testing.T) {
|
func TestDislikeAlbum(t *testing.T) {
|
||||||
t.Run("dislike not existing album", func(t *testing.T) {
|
t.Run("dislike not existing album", func(t *testing.T) {
|
||||||
app, router, ctx := NewApiTest()
|
app, router, conf := NewApiTest()
|
||||||
|
|
||||||
LikeAlbum(router, ctx)
|
LikeAlbum(router, conf)
|
||||||
|
|
||||||
result := PerformRequest(app, "DELETE", "/api/v1/albums/5678/like")
|
result := PerformRequest(app, "DELETE", "/api/v1/albums/5678/like")
|
||||||
t.Log(result.Body)
|
t.Log(result.Body)
|
||||||
|
|
16
internal/api/api.go
Normal file
16
internal/api/api.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
This package contains the PhotoPrism REST api.
|
||||||
|
|
||||||
|
Additional information can be found in our Developer Guide:
|
||||||
|
|
||||||
|
https://github.com/photoprism/photoprism/wiki
|
||||||
|
*/
|
||||||
|
package api
|
||||||
|
|
||||||
|
import "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
var log *logrus.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logrus.StandardLogger()
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/models"
|
"github.com/photoprism/photoprism/internal/models"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
@ -21,6 +20,11 @@ type BatchParams struct {
|
||||||
// POST /api/v1/batch/photos/delete
|
// POST /api/v1/batch/photos/delete
|
||||||
func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) {
|
func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/batch/photos/delete", func(c *gin.Context) {
|
router.POST("/batch/photos/delete", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
var params BatchParams
|
var params BatchParams
|
||||||
|
@ -52,6 +56,11 @@ func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// POST /api/v1/batch/photos/private
|
// POST /api/v1/batch/photos/private
|
||||||
func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
|
func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/batch/photos/private", func(c *gin.Context) {
|
router.POST("/batch/photos/private", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
var params BatchParams
|
var params BatchParams
|
||||||
|
@ -82,6 +91,11 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// POST /api/v1/batch/photos/story
|
// POST /api/v1/batch/photos/story
|
||||||
func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
|
func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/batch/photos/story", func(c *gin.Context) {
|
router.POST("/batch/photos/story", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
var params BatchParams
|
var params BatchParams
|
||||||
|
@ -103,7 +117,6 @@ func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
|
||||||
db.Model(models.Photo{}).Where("id IN (?)", params.Ids).Updates(map[string]interface{}{
|
db.Model(models.Photo{}).Where("id IN (?)", params.Ids).Updates(map[string]interface{}{
|
||||||
"photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"),
|
"photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"),
|
||||||
"photo_private": "0",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
|
|
@ -7,5 +7,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrReadOnly = gin.H{"error": util.UcFirst(config.ErrReadOnly.Error())}
|
ErrReadOnly = gin.H{"code": 403, "error": util.UcFirst(config.ErrReadOnly.Error())}
|
||||||
|
ErrUnauthorized = gin.H{"code": 401, "error": util.UcFirst(config.ErrUnauthorized.Error())}
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,10 +3,11 @@ package api
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
log "github.com/sirupsen/logrus"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
@ -34,6 +35,11 @@ func Import(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
path := conf.ImportPath()
|
path := conf.ImportPath()
|
||||||
|
|
||||||
|
@ -42,14 +48,19 @@ func Import(router *gin.RouterGroup, conf *config.Config) {
|
||||||
path = path + subPath
|
path = path + subPath
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("importing photos from %s", path)
|
event.Info(fmt.Sprintf("importing photos from \"%s\"", filepath.Base(path)))
|
||||||
|
|
||||||
initImporter(conf)
|
initImporter(conf)
|
||||||
|
|
||||||
importer.ImportPhotosFromDirectory(path)
|
importer.ImportPhotosFromDirectory(path)
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := int(time.Since(start).Seconds())
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("completed import in %s", elapsed)})
|
event.Success(fmt.Sprintf("import completed in %d s", elapsed))
|
||||||
|
event.Publish("import.completed", event.Data{"path": path, "seconds": elapsed})
|
||||||
|
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
|
||||||
|
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("import completed in %d s", elapsed)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ package api
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,17 +27,26 @@ func initIndexer(conf *config.Config) {
|
||||||
// POST /api/v1/index
|
// POST /api/v1/index
|
||||||
func Index(router *gin.RouterGroup, conf *config.Config) {
|
func Index(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/index", func(c *gin.Context) {
|
router.POST("/index", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
path := conf.OriginalsPath()
|
path := conf.OriginalsPath()
|
||||||
|
|
||||||
log.Infof("indexing photos in %s", path)
|
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))
|
||||||
|
|
||||||
initIndexer(conf)
|
initIndexer(conf)
|
||||||
|
|
||||||
indexer.IndexAll()
|
indexer.IndexAll()
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := int(time.Since(start).Seconds())
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("indexing completed in %s", elapsed)})
|
event.Success(fmt.Sprintf("indexing completed in %d s", elapsed))
|
||||||
|
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
|
||||||
|
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("indexing completed in %d s", elapsed)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
c.Header("X-Result-Count", strconv.Itoa(form.Count))
|
||||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
c.Header("X-Result-Offset", strconv.Itoa(form.Offset))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
})
|
})
|
||||||
|
@ -44,6 +44,11 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// slug: string Label slug name
|
// slug: string Label slug name
|
||||||
func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/labels/:slug/like", func(c *gin.Context) {
|
router.POST("/labels/:slug/like", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
||||||
|
|
||||||
label, err := search.FindLabelBySlug(c.Param("slug"))
|
label, err := search.FindLabelBySlug(c.Param("slug"))
|
||||||
|
@ -66,6 +71,11 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// slug: string Label slug name
|
// slug: string Label slug name
|
||||||
func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.DELETE("/labels/:slug/like", func(c *gin.Context) {
|
router.DELETE("/labels/:slug/like", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
||||||
|
|
||||||
label, err := search.FindLabelBySlug(c.Param("slug"))
|
label, err := search.FindLabelBySlug(c.Param("slug"))
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
@ -46,8 +45,8 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
c.Header("X-Result-Count", strconv.Itoa(form.Count))
|
||||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
c.Header("X-Result-Offset", strconv.Itoa(form.Offset))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
})
|
})
|
||||||
|
@ -59,6 +58,11 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// id: int Photo ID as returned by the API
|
// id: int Photo ID as returned by the API
|
||||||
func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.POST("/photos/:id/like", func(c *gin.Context) {
|
router.POST("/photos/:id/like", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
||||||
photoID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
photoID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
|
||||||
|
@ -88,6 +92,11 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||||
// id: int Photo ID as returned by the API
|
// id: int Photo ID as returned by the API
|
||||||
func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||||
router.DELETE("/photos/:id/like", func(c *gin.Context) {
|
router.DELETE("/photos/:id/like", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
search := photoprism.NewSearch(conf.OriginalsPath(), conf.Db())
|
||||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||||
|
|
||||||
|
|
75
internal/api/session.go
Normal file
75
internal/api/session.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateSessionParams struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/session
|
||||||
|
func CreateSession(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.POST("/session", func(c *gin.Context) {
|
||||||
|
var params CreateSessionParams
|
||||||
|
|
||||||
|
if err := c.BindJSON(¶ms); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Password != conf.AdminPassword() {
|
||||||
|
c.AbortWithStatusJSON(400, gin.H{"error": "Invalid password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _ := util.RandomToken(16)
|
||||||
|
|
||||||
|
c.Header("X-Session-Token", token)
|
||||||
|
|
||||||
|
gc := conf.Cache()
|
||||||
|
|
||||||
|
gc.Set(token, 1, cache.DefaultExpiration);
|
||||||
|
|
||||||
|
s := gin.H{"token": token, "user": gin.H{"ID": 1, "FirstName": "Admin", "LastName": "", "Role": "admin", "Email": "photoprism@localhost"}}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/v1/session/
|
||||||
|
func DeleteSession(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.DELETE("/session/:token", func(c *gin.Context) {
|
||||||
|
token := c.Param("token")
|
||||||
|
|
||||||
|
gc := conf.Cache()
|
||||||
|
|
||||||
|
gc.Delete(token)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "token": token})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true, if user doesn't have a valid session token
|
||||||
|
func Unauthorized(c *gin.Context, conf *config.Config) bool {
|
||||||
|
// Always return false if site is public
|
||||||
|
if conf.Public() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session token from HTTP header
|
||||||
|
token := c.GetHeader("X-Session-Token")
|
||||||
|
log.Debugf("X-Session-Token: %s", token)
|
||||||
|
|
||||||
|
// Check if session token is valid
|
||||||
|
gc := conf.Cache()
|
||||||
|
_, found := gc.Get(token)
|
||||||
|
|
||||||
|
return !found
|
||||||
|
}
|
50
internal/api/settings.go
Normal file
50
internal/api/settings.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/v1/settings
|
||||||
|
func GetSettings(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.GET("/settings", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := conf.Settings()
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/v1/settings
|
||||||
|
func SaveSettings(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.POST("/settings", func(c *gin.Context) {
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := conf.Settings()
|
||||||
|
|
||||||
|
if err := c.BindJSON(s); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": util.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.WriteValuesToFile(conf.SettingsFile()); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Publish("config.updated", event.Data(conf.ClientConfig()))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "saved"})
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +21,11 @@ func Upload(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if Unauthorized(c, conf) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
subPath := c.Param("path")
|
subPath := c.Param("path")
|
||||||
|
|
||||||
|
|
79
internal/api/websocket.go
Normal file
79
internal/api/websocket.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wsConnection = websocket.Upgrader{}
|
||||||
|
var wsTimeout = 60 * time.Second
|
||||||
|
|
||||||
|
func wsReader(ws *websocket.Conn) {
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
ws.SetReadLimit(512)
|
||||||
|
ws.SetReadDeadline(time.Now().Add(wsTimeout))
|
||||||
|
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(wsTimeout)); return nil })
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, m, err := ws.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Infof("received: %s", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsWriter(ws *websocket.Conn, conf *config.Config) {
|
||||||
|
pingTicker := time.NewTicker(10 * time.Second)
|
||||||
|
s := event.Subscribe("notify.*", "index.*", "upload.*", "import.*", "config.*")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
pingTicker.Stop()
|
||||||
|
event.Unsubscribe(s)
|
||||||
|
ws.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-pingTicker.C:
|
||||||
|
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case msg := <-s.Receiver:
|
||||||
|
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
|
||||||
|
if err := ws.WriteJSON(gin.H{"event": msg.Name, "data": msg.Fields}); err != nil {
|
||||||
|
log.Errorf("write json: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/v1/ws
|
||||||
|
func Websocket(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
router.GET("/ws", func(c *gin.Context) {
|
||||||
|
w := c.Writer
|
||||||
|
r := c.Request
|
||||||
|
|
||||||
|
ws, err := wsConnection.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("upgrade error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
log.Infof("websocket connected: %s", c.Request.RemoteAddr)
|
||||||
|
|
||||||
|
go wsWriter(ws, conf)
|
||||||
|
|
||||||
|
wsReader(ws)
|
||||||
|
})
|
||||||
|
}
|
|
@ -23,6 +23,8 @@ func configAction(ctx *cli.Context) error {
|
||||||
fmt.Printf("copyright %s\n", conf.Copyright())
|
fmt.Printf("copyright %s\n", conf.Copyright())
|
||||||
fmt.Printf("debug %t\n", conf.Debug())
|
fmt.Printf("debug %t\n", conf.Debug())
|
||||||
fmt.Printf("read-only %t\n", conf.ReadOnly())
|
fmt.Printf("read-only %t\n", conf.ReadOnly())
|
||||||
|
fmt.Printf("public %t\n", conf.Public())
|
||||||
|
fmt.Printf("admin-password %s\n", conf.AdminPassword())
|
||||||
fmt.Printf("log-level %s\n", conf.LogLevel())
|
fmt.Printf("log-level %s\n", conf.LogLevel())
|
||||||
fmt.Printf("log-filename %s\n", conf.LogFilename())
|
fmt.Printf("log-filename %s\n", conf.LogFilename())
|
||||||
fmt.Printf("pid-filename %s\n", conf.PIDFilename())
|
fmt.Printf("pid-filename %s\n", conf.PIDFilename())
|
||||||
|
|
|
@ -17,10 +17,12 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
tensorflow "github.com/tensorflow/tensorflow/tensorflow/go"
|
tensorflow "github.com/tensorflow/tensorflow/tensorflow/go"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
gc "github.com/patrickmn/go-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
cache *gc.Cache
|
||||||
config *Params
|
config *Params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,11 +203,25 @@ func (c *Config) Debug() bool {
|
||||||
return c.config.Debug
|
return c.config.Debug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public returns true if app requires no authentication.
|
||||||
|
func (c *Config) Public() bool {
|
||||||
|
return c.config.Public
|
||||||
|
}
|
||||||
|
|
||||||
// ReadOnly returns true if photo directories are write protected.
|
// ReadOnly returns true if photo directories are write protected.
|
||||||
func (c *Config) ReadOnly() bool {
|
func (c *Config) ReadOnly() bool {
|
||||||
return c.config.ReadOnly
|
return c.config.ReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminPassword returns the admin password.
|
||||||
|
func (c *Config) AdminPassword() string {
|
||||||
|
if c.config.AdminPassword == "" {
|
||||||
|
return "photoprism"
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.config.AdminPassword
|
||||||
|
}
|
||||||
|
|
||||||
// LogLevel returns the logrus log level.
|
// LogLevel returns the logrus log level.
|
||||||
func (c *Config) LogLevel() log.Level {
|
func (c *Config) LogLevel() log.Level {
|
||||||
if c.Debug() {
|
if c.Debug() {
|
||||||
|
@ -224,6 +240,11 @@ func (c *Config) ConfigFile() string {
|
||||||
return c.config.ConfigFile
|
return c.config.ConfigFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SettingsFile returns the user settings file name.
|
||||||
|
func (c *Config) SettingsFile() string {
|
||||||
|
return c.ConfigPath() + "/settings.yml"
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigPath returns the config path.
|
// ConfigPath returns the config path.
|
||||||
func (c *Config) ConfigPath() string {
|
func (c *Config) ConfigPath() string {
|
||||||
if c.config.ConfigPath == "" {
|
if c.config.ConfigPath == "" {
|
||||||
|
@ -431,6 +452,15 @@ func (c *Config) HttpStaticBuildPath() string {
|
||||||
return c.HttpStaticPath() + "/build"
|
return c.HttpStaticPath() + "/build"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache returns the in-memory cache.
|
||||||
|
func (c *Config) Cache() *gc.Cache {
|
||||||
|
if c.cache == nil {
|
||||||
|
c.cache = gc.New(336*time.Hour, 30*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.cache
|
||||||
|
}
|
||||||
|
|
||||||
// Db returns the db connection.
|
// Db returns the db connection.
|
||||||
func (c *Config) Db() *gorm.DB {
|
func (c *Config) Db() *gorm.DB {
|
||||||
if c.db == nil {
|
if c.db == nil {
|
||||||
|
@ -501,11 +531,13 @@ func (c *Config) ClientConfig() ClientConfig {
|
||||||
"copyright": c.Copyright(),
|
"copyright": c.Copyright(),
|
||||||
"debug": c.Debug(),
|
"debug": c.Debug(),
|
||||||
"readonly": c.ReadOnly(),
|
"readonly": c.ReadOnly(),
|
||||||
|
"public": c.Public(),
|
||||||
"cameras": cameras,
|
"cameras": cameras,
|
||||||
"countries": countries,
|
"countries": countries,
|
||||||
"thumbnails": Thumbnails,
|
"thumbnails": Thumbnails,
|
||||||
"jsHash": jsHash,
|
"jsHash": jsHash,
|
||||||
"cssHash": cssHash,
|
"cssHash": cssHash,
|
||||||
|
"settings": c.Settings(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -516,6 +548,7 @@ func (c *Config) Init(ctx context.Context) error {
|
||||||
return c.connectToDatabase(ctx)
|
return c.connectToDatabase(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown closes open database connections.
|
||||||
func (c *Config) Shutdown() {
|
func (c *Config) Shutdown() {
|
||||||
if err := c.CloseDb(); err != nil {
|
if err := c.CloseDb(); err != nil {
|
||||||
log.Errorf("could not close database connection: %s", err)
|
log.Errorf("could not close database connection: %s", err)
|
||||||
|
@ -523,3 +556,15 @@ func (c *Config) Shutdown() {
|
||||||
log.Info("closed database connection")
|
log.Info("closed database connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings returns the current user settings.
|
||||||
|
func (c *Config) Settings() *Settings {
|
||||||
|
s := NewSettings()
|
||||||
|
p := c.SettingsFile()
|
||||||
|
|
||||||
|
if err := s.SetValuesFromFile(p); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
|
@ -6,4 +6,5 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrReadOnly = errors.New("not available in read-only mode")
|
ErrReadOnly = errors.New("not available in read-only mode")
|
||||||
|
ErrUnauthorized = errors.New("please log in and try again")
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,17 @@ var GlobalFlags = []cli.Flag{
|
||||||
Usage: "run in read-only mode",
|
Usage: "run in read-only mode",
|
||||||
EnvVar: "PHOTOPRISM_READ_ONLY",
|
EnvVar: "PHOTOPRISM_READ_ONLY",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "public",
|
||||||
|
Usage: "no authentication required",
|
||||||
|
EnvVar: "PHOTOPRISM_PUBLIC",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "admin-password",
|
||||||
|
Usage: "admin password",
|
||||||
|
Value: "photoprism",
|
||||||
|
EnvVar: "PHOTOPRISM_ADMIN_PASSWORD",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "log-level, l",
|
Name: "log-level, l",
|
||||||
Usage: "trace, debug, info, warning, error, fatal or panic",
|
Usage: "trace, debug, info, warning, error, fatal or panic",
|
||||||
|
@ -132,11 +143,6 @@ var GlobalFlags = []cli.Flag{
|
||||||
Usage: "debug, release or test",
|
Usage: "debug, release or test",
|
||||||
EnvVar: "PHOTOPRISM_HTTP_MODE",
|
EnvVar: "PHOTOPRISM_HTTP_MODE",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
|
||||||
Name: "http-password",
|
|
||||||
Usage: "HTTP server password (optional)",
|
|
||||||
EnvVar: "PHOTOPRISM_HTTP_PASSWORD",
|
|
||||||
},
|
|
||||||
cli.IntFlag{
|
cli.IntFlag{
|
||||||
Name: "sql-port, s",
|
Name: "sql-port, s",
|
||||||
Usage: "built-in SQL server port",
|
Usage: "built-in SQL server port",
|
||||||
|
|
|
@ -33,6 +33,8 @@ type Params struct {
|
||||||
Copyright string
|
Copyright string
|
||||||
Debug bool `yaml:"debug" flag:"debug"`
|
Debug bool `yaml:"debug" flag:"debug"`
|
||||||
ReadOnly bool `yaml:"read-only" flag:"read-only"`
|
ReadOnly bool `yaml:"read-only" flag:"read-only"`
|
||||||
|
Public bool `yaml:"public" flag:"public"`
|
||||||
|
AdminPassword string `yaml:"admin-password" flag:"admin-password"`
|
||||||
LogLevel string `yaml:"log-level" flag:"log-level"`
|
LogLevel string `yaml:"log-level" flag:"log-level"`
|
||||||
ConfigFile string
|
ConfigFile string
|
||||||
ConfigPath string `yaml:"config-path" flag:"config-path"`
|
ConfigPath string `yaml:"config-path" flag:"config-path"`
|
||||||
|
|
50
internal/config/settings.go
Normal file
50
internal/config/settings.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Theme string `json:"theme" yaml:"theme" flag:"theme"`
|
||||||
|
Language string `json:"language" yaml:"language" flag:"language"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettings() *Settings {
|
||||||
|
return &Settings{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
||||||
|
func (s *Settings) SetValuesFromFile(fileName string) error {
|
||||||
|
if !util.Exists(fileName) {
|
||||||
|
return fmt.Errorf("settings file not found: \"%s\"", fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlConfig, err := ioutil.ReadFile(fileName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return yaml.Unmarshal(yamlConfig, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteValuesToFile uses a yaml config file to initiate the configuration entity.
|
||||||
|
func (s *Settings) WriteValuesToFile(fileName string) error {
|
||||||
|
if !util.Exists(fileName) {
|
||||||
|
return fmt.Errorf("settings file not found: \"%s\"", fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := yaml.Marshal(s)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.WriteFile(fileName, data, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ func NewTestParams() *Params {
|
||||||
testDataPath := testDataPath(assetsPath)
|
testDataPath := testDataPath(assetsPath)
|
||||||
|
|
||||||
c := &Params{
|
c := &Params{
|
||||||
|
Public: true,
|
||||||
|
ReadOnly: false,
|
||||||
DarktableBin: "/usr/bin/darktable-cli",
|
DarktableBin: "/usr/bin/darktable-cli",
|
||||||
AssetsPath: assetsPath,
|
AssetsPath: assetsPath,
|
||||||
CachePath: testDataPath + "/cache",
|
CachePath: testDataPath + "/cache",
|
||||||
|
@ -40,7 +42,7 @@ func NewTestParams() *Params {
|
||||||
ImportPath: testDataPath + "/import",
|
ImportPath: testDataPath + "/import",
|
||||||
ExportPath: testDataPath + "/export",
|
ExportPath: testDataPath + "/export",
|
||||||
DatabaseDriver: "mysql",
|
DatabaseDriver: "mysql",
|
||||||
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-mysql:4001)/photoprism?parseTime=true",
|
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-db:4001)/photoprism?parseTime=true",
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
@ -59,7 +61,7 @@ func NewTestParamsError() *Params {
|
||||||
ImportPath: testDataPath + "/import",
|
ImportPath: testDataPath + "/import",
|
||||||
ExportPath: testDataPath + "/export",
|
ExportPath: testDataPath + "/export",
|
||||||
DatabaseDriver: "mysql",
|
DatabaseDriver: "mysql",
|
||||||
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-mysql:4001)/photoprism?parseTime=true",
|
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-db:4001)/photoprism?parseTime=true",
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
61
internal/event/hub.go
Normal file
61
internal/event/hub.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/leandro-lugaresi/hub"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hub = hub.Hub
|
||||||
|
type Data = hub.Fields
|
||||||
|
type Message = hub.Message
|
||||||
|
var log *logrus.Logger
|
||||||
|
var channelCap = 10
|
||||||
|
var sharedHub = NewHub()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = logrus.StandardLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHub () *Hub {
|
||||||
|
return hub.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SharedHub() *Hub {
|
||||||
|
return sharedHub
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(msg string) {
|
||||||
|
log.Error(msg)
|
||||||
|
Publish("notify.error", Data{"msg": msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Success(msg string) {
|
||||||
|
log.Info(msg)
|
||||||
|
Publish("notify.success", Data{"msg": msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(msg string) {
|
||||||
|
log.Info(msg)
|
||||||
|
Publish("notify.info", Data{"msg": msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warning(msg string) {
|
||||||
|
log.Warn(msg)
|
||||||
|
Publish("notify.warning", Data{"msg": msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Publish (event string, data Data) {
|
||||||
|
log.Infof("publish %s: %v", event, data)
|
||||||
|
SharedHub().Publish(Message{
|
||||||
|
Name: event,
|
||||||
|
Fields: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Subscribe(topics ...string) hub.Subscription {
|
||||||
|
return SharedHub().Subscribe(channelCap, topics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unsubscribe(s hub.Subscription) {
|
||||||
|
SharedHub().Unsubscribe(s)
|
||||||
|
}
|
31
internal/event/hub_test.go
Normal file
31
internal/event/hub_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/leandro-lugaresi/hub"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSharedHub(t *testing.T) {
|
||||||
|
h := SharedHub()
|
||||||
|
|
||||||
|
assert.IsType(t, &hub.Hub{}, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublishSubscribe(t *testing.T) {
|
||||||
|
s := Subscribe("foo.bar")
|
||||||
|
|
||||||
|
assert.IsType(t, hub.Subscription{}, s)
|
||||||
|
|
||||||
|
Publish("foo.bar", Data{"id": 13})
|
||||||
|
|
||||||
|
msg := <-s.Receiver
|
||||||
|
|
||||||
|
t.Logf("receive msg with topic %s: %v\n", msg.Name, msg.Fields)
|
||||||
|
|
||||||
|
assert.Equal(t, "foo.bar", msg.Name)
|
||||||
|
assert.Equal(t, Data{"id": 13}, msg.Fields)
|
||||||
|
|
||||||
|
Unsubscribe(s)
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ type Photo struct {
|
||||||
PhotoAltitude int
|
PhotoAltitude int
|
||||||
PhotoFocalLength int
|
PhotoFocalLength int
|
||||||
PhotoIso int
|
PhotoIso int
|
||||||
PhotoAperture float64
|
PhotoFNumber float64
|
||||||
PhotoExposure string
|
PhotoExposure string
|
||||||
PhotoViews uint
|
PhotoViews uint
|
||||||
Camera *Camera
|
Camera *Camera
|
||||||
|
|
|
@ -21,11 +21,14 @@ type Exif struct {
|
||||||
Artist string
|
Artist string
|
||||||
CameraMake string
|
CameraMake string
|
||||||
CameraModel string
|
CameraModel string
|
||||||
|
Description string
|
||||||
LensMake string
|
LensMake string
|
||||||
LensModel string
|
LensModel string
|
||||||
|
Flash bool
|
||||||
FocalLength int
|
FocalLength int
|
||||||
Exposure string
|
Exposure string
|
||||||
Aperture float64
|
Aperture float64
|
||||||
|
FNumber float64
|
||||||
Iso int
|
Iso int
|
||||||
Lat float64
|
Lat float64
|
||||||
Long float64
|
Long float64
|
||||||
|
@ -148,6 +151,17 @@ func (m *MediaFile) Exif() (result *Exif, err error) {
|
||||||
m.exifData.Exposure = value
|
m.exifData.Exposure = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if value, ok := tags["FNumber"]; ok {
|
||||||
|
values := strings.Split(value, "/")
|
||||||
|
|
||||||
|
if len(values) == 2 && values[1] != "0" && values[1] != "" {
|
||||||
|
number, _ := strconv.ParseFloat(values[0], 64)
|
||||||
|
denom, _ := strconv.ParseFloat(values[1], 64)
|
||||||
|
|
||||||
|
m.exifData.FNumber = math.Round((number/denom)*1000) / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if value, ok := tags["ApertureValue"]; ok {
|
if value, ok := tags["ApertureValue"]; ok {
|
||||||
values := strings.Split(value, "/")
|
values := strings.Split(value, "/")
|
||||||
|
|
||||||
|
@ -246,6 +260,16 @@ func (m *MediaFile) Exif() (result *Exif, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if value, ok := tags["Flash"]; ok {
|
||||||
|
if i, err := strconv.Atoi(value); err == nil && i&1 == 1 {
|
||||||
|
m.exifData.Flash = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := tags["ImageDescription"]; ok {
|
||||||
|
m.exifData.Description = strings.Replace(value, "\"", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
m.exifData.All = tags
|
m.exifData.All = tags
|
||||||
|
|
||||||
return m.exifData, nil
|
return m.exifData, nil
|
||||||
|
|
|
@ -11,20 +11,75 @@ import (
|
||||||
func TestMediaFile_Exif_JPEG(t *testing.T) {
|
func TestMediaFile_Exif_JPEG(t *testing.T) {
|
||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
|
|
||||||
img, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
t.Run("elephants.jpg", func(t *testing.T) {
|
||||||
|
img, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
info, err := img.Exif()
|
info, err := img.Exif()
|
||||||
|
|
||||||
assert.Empty(t, err)
|
assert.Empty(t, err)
|
||||||
|
|
||||||
assert.IsType(t, &Exif{}, info)
|
assert.IsType(t, &Exif{}, info)
|
||||||
|
|
||||||
assert.Equal(t, "Canon EOS 6D", info.CameraModel)
|
assert.Equal(t, "", info.UUID)
|
||||||
assert.Equal(t, "Africa/Johannesburg", info.TimeZone)
|
assert.Equal(t, "2013-11-26 13:53:55 +0000 UTC", info.TakenAt.String())
|
||||||
t.Logf("UTC: %s", info.TakenAt.String())
|
assert.Equal(t, "2013-11-26 15:53:55 +0000 UTC", info.TakenAtLocal.String())
|
||||||
t.Logf("Local: %s", info.TakenAtLocal.String())
|
assert.Equal(t, 1, info.Orientation)
|
||||||
|
assert.Equal(t, "Canon EOS 6D", info.CameraModel)
|
||||||
|
assert.Equal(t, "Canon", info.CameraMake)
|
||||||
|
assert.Equal(t, "EF70-200mm f/4L IS USM", info.LensModel)
|
||||||
|
assert.Equal(t, "", info.LensMake)
|
||||||
|
assert.Equal(t, "Africa/Johannesburg", info.TimeZone)
|
||||||
|
assert.Equal(t, "", info.Artist)
|
||||||
|
assert.Equal(t, 111, info.FocalLength)
|
||||||
|
assert.Equal(t, "1/640", info.Exposure)
|
||||||
|
assert.Equal(t, 6.644, info.Aperture)
|
||||||
|
assert.Equal(t, 10.0, info.FNumber)
|
||||||
|
assert.Equal(t, 200, info.Iso)
|
||||||
|
assert.Equal(t, -33.45347, info.Lat)
|
||||||
|
assert.Equal(t, 25.764645, info.Long)
|
||||||
|
assert.Equal(t, 190, info.Altitude)
|
||||||
|
assert.Equal(t, 1365, info.Width)
|
||||||
|
assert.Equal(t, 0, info.Height)
|
||||||
|
assert.Equal(t, false, info.Flash)
|
||||||
|
assert.Equal(t, "", info.Description)
|
||||||
|
t.Logf("UTC: %s", info.TakenAt.String())
|
||||||
|
t.Logf("Local: %s", info.TakenAtLocal.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fern_green.jpg", func(t *testing.T) {
|
||||||
|
img, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg")
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
info, err := img.Exif()
|
||||||
|
|
||||||
|
assert.Empty(t, err)
|
||||||
|
|
||||||
|
assert.IsType(t, &Exif{}, info)
|
||||||
|
|
||||||
|
assert.Equal(t, "", info.UUID)
|
||||||
|
assert.Equal(t, 1, info.Orientation)
|
||||||
|
assert.Equal(t, "Canon EOS 7D", info.CameraModel)
|
||||||
|
assert.Equal(t, "Canon", info.CameraMake)
|
||||||
|
assert.Equal(t, "EF100mm f/2.8L Macro IS USM", info.LensModel)
|
||||||
|
assert.Equal(t, "", info.LensMake)
|
||||||
|
assert.Equal(t, "", info.TimeZone)
|
||||||
|
assert.Equal(t, "", info.Artist)
|
||||||
|
assert.Equal(t, 100, info.FocalLength)
|
||||||
|
assert.Equal(t, "1/250", info.Exposure)
|
||||||
|
assert.Equal(t, 6.644, info.Aperture)
|
||||||
|
assert.Equal(t, 10.0, info.FNumber)
|
||||||
|
assert.Equal(t, 200, info.Iso)
|
||||||
|
assert.Equal(t, 0, info.Altitude)
|
||||||
|
assert.Equal(t, 2048, info.Width)
|
||||||
|
assert.Equal(t, 0, info.Height)
|
||||||
|
assert.Equal(t, true, info.Flash)
|
||||||
|
assert.Equal(t, "", info.Description)
|
||||||
|
t.Logf("UTC: %s", info.TakenAt.String())
|
||||||
|
t.Logf("Local: %s", info.TakenAtLocal.String())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaFile_Exif_DNG(t *testing.T) {
|
func TestMediaFile_Exif_DNG(t *testing.T) {
|
||||||
|
@ -44,7 +99,25 @@ func TestMediaFile_Exif_DNG(t *testing.T) {
|
||||||
|
|
||||||
assert.IsType(t, &Exif{}, info)
|
assert.IsType(t, &Exif{}, info)
|
||||||
|
|
||||||
|
assert.Equal(t, "", info.UUID)
|
||||||
|
assert.Equal(t, "2019-06-06 07:29:51 +0000 UTC", info.TakenAt.String())
|
||||||
|
assert.Equal(t, "2019-06-06 07:29:51 +0000 UTC", info.TakenAtLocal.String())
|
||||||
|
assert.Equal(t, 1, info.Orientation)
|
||||||
assert.Equal(t, "Canon EOS 6D", info.CameraModel)
|
assert.Equal(t, "Canon EOS 6D", info.CameraModel)
|
||||||
|
assert.Equal(t, "Canon", info.CameraMake)
|
||||||
|
assert.Equal(t, "EF24-105mm f/4L IS USM", info.LensModel)
|
||||||
|
assert.Equal(t, "", info.Artist)
|
||||||
|
assert.Equal(t, 65, info.FocalLength)
|
||||||
|
assert.Equal(t, "1/60", info.Exposure)
|
||||||
|
assert.Equal(t, 4.971, info.Aperture)
|
||||||
|
assert.Equal(t, 1000, info.Iso)
|
||||||
|
assert.Equal(t, 0.0, info.Lat)
|
||||||
|
assert.Equal(t, 0.0, info.Long)
|
||||||
|
assert.Equal(t, 0, info.Altitude)
|
||||||
|
assert.Equal(t, 171, info.Width)
|
||||||
|
assert.Equal(t, 0, info.Height)
|
||||||
|
assert.Equal(t, false, info.Flash)
|
||||||
|
assert.Equal(t, "", info.Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaFile_Exif_HEIF(t *testing.T) {
|
func TestMediaFile_Exif_HEIF(t *testing.T) {
|
||||||
|
@ -76,7 +149,27 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "", jpegInfo.UUID)
|
||||||
|
assert.Equal(t, "2018-09-10 03:16:13 +0000 UTC", jpegInfo.TakenAt.String())
|
||||||
|
assert.Equal(t, "2018-09-10 12:16:13 +0000 UTC", jpegInfo.TakenAtLocal.String())
|
||||||
|
assert.Equal(t, 6, jpegInfo.Orientation)
|
||||||
assert.Equal(t, "iPhone 7", jpegInfo.CameraModel)
|
assert.Equal(t, "iPhone 7", jpegInfo.CameraModel)
|
||||||
|
assert.Equal(t, "Apple", jpegInfo.CameraMake)
|
||||||
|
assert.Equal(t, "iPhone 7 back camera 3.99mm f/1.8", jpegInfo.LensModel)
|
||||||
|
assert.Equal(t, "Apple", jpegInfo.LensMake)
|
||||||
|
assert.Equal(t, "Asia/Tokyo", jpegInfo.TimeZone)
|
||||||
|
assert.Equal(t, "", jpegInfo.Artist)
|
||||||
|
assert.Equal(t, 74, jpegInfo.FocalLength)
|
||||||
|
assert.Equal(t, "1/4000", jpegInfo.Exposure)
|
||||||
|
assert.Equal(t, 1.696, jpegInfo.Aperture)
|
||||||
|
assert.Equal(t, 20, jpegInfo.Iso)
|
||||||
|
assert.Equal(t, 34.79745, jpegInfo.Lat)
|
||||||
|
assert.Equal(t, 134.76463333333334, jpegInfo.Long)
|
||||||
|
assert.Equal(t, 0, jpegInfo.Altitude)
|
||||||
|
assert.Equal(t, 0, jpegInfo.Width)
|
||||||
|
assert.Equal(t, 0, jpegInfo.Height)
|
||||||
|
assert.Equal(t, false, jpegInfo.Flash)
|
||||||
|
assert.Equal(t, "", jpegInfo.Description)
|
||||||
|
|
||||||
if err := os.Remove(conf.ExamplesPath() + "/iphone_7.jpg"); err != nil {
|
if err := os.Remove(conf.ExamplesPath() + "/iphone_7.jpg"); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/models"
|
"github.com/photoprism/photoprism/internal/models"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
@ -78,11 +79,16 @@ func (i *Importer) ImportPhotosFromDirectory(importPath string) {
|
||||||
relatedFiles, mainFile, err := mediaFile.RelatedFiles()
|
relatedFiles, mainFile, err := mediaFile.RelatedFiles()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("could not import \"%s\": %s", mediaFile.RelativeFilename(importPath), err.Error())
|
event.Error(fmt.Sprintf("could not import \"%s\": %s", mediaFile.RelativeFilename(importPath), err.Error()))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.Publish("import.file", event.Data{
|
||||||
|
"fileName": mainFile.Filename(),
|
||||||
|
"baseName": filepath.Base(mainFile.Filename()),
|
||||||
|
})
|
||||||
|
|
||||||
for _, relatedMediaFile := range relatedFiles {
|
for _, relatedMediaFile := range relatedFiles {
|
||||||
relativeFilename := relatedMediaFile.RelativeFilename(importPath)
|
relativeFilename := relatedMediaFile.RelativeFilename(importPath)
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/models"
|
"github.com/photoprism/photoprism/internal/models"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -113,6 +114,12 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
||||||
fileName := mediaFile.RelativeFilename(i.originalsPath())
|
fileName := mediaFile.RelativeFilename(i.originalsPath())
|
||||||
fileHash := mediaFile.Hash()
|
fileHash := mediaFile.Hash()
|
||||||
|
|
||||||
|
event.Publish("index.file", event.Data{
|
||||||
|
"fileHash": fileHash,
|
||||||
|
"fileName": fileName,
|
||||||
|
"baseName": filepath.Base(fileName),
|
||||||
|
})
|
||||||
|
|
||||||
exifData, err := mediaFile.Exif()
|
exifData, err := mediaFile.Exif()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -156,11 +163,11 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Camera, Lens, Focal Length and Aperture
|
// Set Camera, Lens, Focal Length and F Number
|
||||||
photo.Camera = models.NewCamera(mediaFile.CameraModel(), mediaFile.CameraMake()).FirstOrCreate(i.db)
|
photo.Camera = models.NewCamera(mediaFile.CameraModel(), mediaFile.CameraMake()).FirstOrCreate(i.db)
|
||||||
photo.Lens = models.NewLens(mediaFile.LensModel(), mediaFile.LensMake()).FirstOrCreate(i.db)
|
photo.Lens = models.NewLens(mediaFile.LensModel(), mediaFile.LensMake()).FirstOrCreate(i.db)
|
||||||
photo.PhotoFocalLength = mediaFile.FocalLength()
|
photo.PhotoFocalLength = mediaFile.FocalLength()
|
||||||
photo.PhotoAperture = mediaFile.Aperture()
|
photo.PhotoFNumber = mediaFile.FNumber()
|
||||||
photo.PhotoIso = mediaFile.Iso()
|
photo.PhotoIso = mediaFile.Iso()
|
||||||
photo.PhotoExposure = mediaFile.Exposure()
|
photo.PhotoExposure = mediaFile.Exposure()
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,14 +156,14 @@ func (m *MediaFile) FocalLength() int {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aperture returns the aperture with which the media file was created.
|
// FNumber returns the F number with which the media file was created.
|
||||||
func (m *MediaFile) Aperture() float64 {
|
func (m *MediaFile) FNumber() float64 {
|
||||||
info, err := m.Exif()
|
info, err := m.Exif()
|
||||||
|
|
||||||
var result float64
|
var result float64
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
result = info.Aperture
|
result = info.FNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -136,20 +136,20 @@ func TestMediaFile_FocalLength(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaFile_Aperture(t *testing.T) {
|
func TestMediaFile_FNumber(t *testing.T) {
|
||||||
t.Run("/cat_brown.jpg", func(t *testing.T) {
|
t.Run("/cat_brown.jpg", func(t *testing.T) {
|
||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
|
|
||||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg")
|
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 2.275, mediaFile.Aperture())
|
assert.Equal(t, 2.2, mediaFile.FNumber())
|
||||||
})
|
})
|
||||||
t.Run("/elephants.jpg", func(t *testing.T) {
|
t.Run("/elephants.jpg", func(t *testing.T) {
|
||||||
conf := config.TestConfig()
|
conf := config.TestConfig()
|
||||||
|
|
||||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 6.644, mediaFile.Aperture())
|
assert.Equal(t, 10.0, mediaFile.FNumber())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -171,11 +171,11 @@ func (s *Search) Photos(form forms.PhotoSearchForm) (results []PhotoSearchResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Fmin > 0 {
|
if form.Fmin > 0 {
|
||||||
q = q.Where("photos.photo_aperture >= ?", form.Fmin)
|
q = q.Where("photos.photo_f_number >= ?", form.Fmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Fmax > 0 {
|
if form.Fmax > 0 {
|
||||||
q = q.Where("photos.photo_aperture <= ?", form.Fmax)
|
q = q.Where("photos.photo_f_number <= ?", form.Fmax)
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Dist == 0 {
|
if form.Dist == 0 {
|
||||||
|
|
|
@ -20,12 +20,17 @@ type PhotoSearchResult struct {
|
||||||
PhotoKeywords string
|
PhotoKeywords string
|
||||||
PhotoColors string
|
PhotoColors string
|
||||||
PhotoColor string
|
PhotoColor string
|
||||||
PhotoLat float64
|
|
||||||
PhotoLong float64
|
|
||||||
PhotoFavorite bool
|
PhotoFavorite bool
|
||||||
PhotoPrivate bool
|
PhotoPrivate bool
|
||||||
PhotoSensitive bool
|
PhotoSensitive bool
|
||||||
PhotoStory bool
|
PhotoStory bool
|
||||||
|
PhotoLat float64
|
||||||
|
PhotoLong float64
|
||||||
|
PhotoAltitude int
|
||||||
|
PhotoFocalLength int
|
||||||
|
PhotoIso int
|
||||||
|
PhotoFNumber float64
|
||||||
|
PhotoExposure string
|
||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
CameraID uint
|
CameraID uint
|
||||||
|
|
|
@ -15,9 +15,18 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||||
// Static assets like js and css files
|
// Static assets like js and css files
|
||||||
router.Static("/static", conf.HttpStaticPath())
|
router.Static("/static", conf.HttpStaticPath())
|
||||||
|
|
||||||
|
// socket.io
|
||||||
|
/* s := router.Group("/socket.io")
|
||||||
|
{
|
||||||
|
api.Socket(s, conf)
|
||||||
|
} */
|
||||||
|
|
||||||
// JSON-REST API Version 1
|
// JSON-REST API Version 1
|
||||||
v1 := router.Group("/api/v1")
|
v1 := router.Group("/api/v1")
|
||||||
{
|
{
|
||||||
|
api.CreateSession(v1, conf)
|
||||||
|
api.DeleteSession(v1, conf)
|
||||||
|
|
||||||
api.GetThumbnail(v1, conf)
|
api.GetThumbnail(v1, conf)
|
||||||
api.GetDownload(v1, conf)
|
api.GetDownload(v1, conf)
|
||||||
|
|
||||||
|
@ -43,6 +52,11 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||||
api.DislikeAlbum(v1, conf)
|
api.DislikeAlbum(v1, conf)
|
||||||
api.AlbumThumbnail(v1, conf)
|
api.AlbumThumbnail(v1, conf)
|
||||||
api.CreateAlbum(v1, conf)
|
api.CreateAlbum(v1, conf)
|
||||||
|
|
||||||
|
api.GetSettings(v1, conf)
|
||||||
|
api.SaveSettings(v1, conf)
|
||||||
|
|
||||||
|
api.Websocket(v1, conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default HTML page (client-side routing implemented via Vue.js)
|
// Default HTML page (client-side routing implemented via Vue.js)
|
||||||
|
|
14
internal/util/token.go
Normal file
14
internal/util/token.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RandomToken(size int) (string, error) {
|
||||||
|
b := make([]byte, size)
|
||||||
|
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x", b), err
|
||||||
|
}
|
Loading…
Reference in a new issue