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
|
||||
WORKDIR "/go/src/github.com/photoprism/photoprism"
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
debug: false
|
||||
read-only: false
|
||||
public: false
|
||||
admin-password: photoprism
|
||||
config-path: ~/.config/photoprism
|
||||
cache-path: ~/.cache/photoprism
|
||||
assets-path: ~/.local/share/photoprism
|
||||
|
@ -12,7 +15,6 @@ sql-password: photoprism
|
|||
http-host:
|
||||
http-mode: release
|
||||
http-port: 2342
|
||||
http-password:
|
||||
database-driver: internal
|
||||
database-dsn: root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true
|
||||
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>
|
||||
window.appConfig = {
|
||||
name: "{{ .name }}",
|
||||
version: "{{ .version }}",
|
||||
copyright: "{{ .copyright }}",
|
||||
debug: {{ .debug }},
|
||||
readonly: {{ .readonly }},
|
||||
cameras: {{ .cameras }},
|
||||
countries: {{ .countries }},
|
||||
thumbnails: {{ .thumbnails }}
|
||||
"name": "{{ .name }}",
|
||||
"version": "{{ .version }}",
|
||||
"copyright": "{{ .copyright }}",
|
||||
"debug": {{ .debug }},
|
||||
"readonly": {{ .readonly }},
|
||||
"public": {{ .public }},
|
||||
"cameras": {{ .cameras }},
|
||||
"countries": {{ .countries }},
|
||||
"thumbnails": {{ .thumbnails }},
|
||||
"settings": {{ .settings }},
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
@ -63,6 +65,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="p-busy-overlay"></div>
|
||||
|
||||
<script src="/static/build/app.js?{{ .jsHash }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -5,7 +5,7 @@ services:
|
|||
build: .
|
||||
image: photoprism/photoprism:develop
|
||||
depends_on:
|
||||
- photoprism-mysql
|
||||
- photoprism-db
|
||||
command: tail -f /dev/null
|
||||
volumes:
|
||||
- "~/.cache/npm:/root/.cache/npm"
|
||||
|
@ -34,8 +34,8 @@ services:
|
|||
CI_BUILD_ID:
|
||||
CI_JOB_ID:
|
||||
|
||||
photoprism-mysql:
|
||||
image: mysql:8.0.16
|
||||
photoprism-db:
|
||||
image: mariadb:10.4.8
|
||||
command: mysqld --port=4001 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=1024
|
||||
expose:
|
||||
- "4001"
|
||||
|
|
|
@ -5,7 +5,7 @@ services:
|
|||
build: .
|
||||
image: photoprism/photoprism:develop
|
||||
depends_on:
|
||||
- photoprism-mysql
|
||||
- photoprism-db
|
||||
ports:
|
||||
- "2342:2342" # Web Server (PhotoPrism)
|
||||
- "4000:4000" # Database (MySQL compatible)
|
||||
|
@ -31,8 +31,8 @@ services:
|
|||
PHOTOPRISM_SQL_PASSWORD: "photoprism"
|
||||
TF_CPP_MIN_LOG_LEVEL: 0
|
||||
|
||||
photoprism-mysql:
|
||||
image: mysql:8.0.16
|
||||
photoprism-db:
|
||||
image: mariadb:10.4.8
|
||||
command: mysqld --port=4001 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=1024
|
||||
expose:
|
||||
- "4001"
|
||||
|
|
|
@ -9,4 +9,4 @@ RUN wget -qO- https://dl.photoprism.org/fixtures/demo.tar.gz | tar xvz -C Pictur
|
|||
RUN photoprism import
|
||||
|
||||
# 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
|
||||
|
||||
# Install Go
|
||||
ENV GOLANG_VERSION 1.13
|
||||
ENV GOLANG_VERSION 1.13.4
|
||||
RUN set -eux; \
|
||||
\
|
||||
url="https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz"; \
|
||||
wget -O go.tgz "$url"; \
|
||||
echo "68a2297eb099d1a76097905a2ce334e3155004ec08cdea85f24527be3c48e856 *go.tgz" | sha256sum -c -; \
|
||||
echo "692d17071736f74be04a72a06dab9cac1cd759377bd85316e52b2227604c004c *go.tgz" | sha256sum -c -; \
|
||||
tar -C /usr/local -xzf go.tgz; \
|
||||
rm go.tgz; \
|
||||
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
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.6.0",
|
||||
"@babel/core": "^7.6.0",
|
||||
"@babel/plugin-transform-runtime": "^7.6.0",
|
||||
"@babel/polyfill": "^7.6.0",
|
||||
"@babel/preset-env": "^7.6.0",
|
||||
"@babel/register": "^7.6.0",
|
||||
"@fortawesome/fontawesome-free": "^5.10.2",
|
||||
"@types/leaflet": "^1.5.1",
|
||||
"@babel/cli": "^7.7.0",
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/plugin-transform-runtime": "^7.6.2",
|
||||
"@babel/polyfill": "^7.7.0",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/register": "^7.7.0",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@types/leaflet": "^1.5.5",
|
||||
"acorn": "^6.3.0",
|
||||
"ajv": "^6.10.2",
|
||||
"autoprefixer": "^9.6.1",
|
||||
"autoprefixer": "^9.7.1",
|
||||
"axios": "^0.19.0",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-istanbul": "^5.2.0",
|
||||
"browserslist": "^4.7.0",
|
||||
"browserslist": "^4.7.2",
|
||||
"chai": "^4.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"chart.js": "^2.5.0",
|
||||
"chrome-finder": "^1.0.5",
|
||||
"chart.js": "^2.9.2",
|
||||
"chrome-finder": "^1.0.6",
|
||||
"clean-webpack-plugin": "^3.0.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",
|
||||
"css-loader": "^2.1.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"eslint": "^6.4.0",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-standard": "^13.0.1",
|
||||
"eslint-formatter-pretty": "^2.1.1",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
|
@ -57,7 +57,7 @@
|
|||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-proxy-middleware": "^0.19.1",
|
||||
"inject-loader": "^4.0.1",
|
||||
"karma": "^4.3.0",
|
||||
"karma": "^4.4.1",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "^2.1.0",
|
||||
"karma-htmlfile-reporter": "^0.3.8",
|
||||
|
@ -65,16 +65,16 @@
|
|||
"karma-verbose-reporter": "^0.0.6",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"leaflet": "^1.5.1",
|
||||
"luxon": "^1.17.3",
|
||||
"luxon": "^1.21.1",
|
||||
"material-design-icons-iconfont": "^5.0.1",
|
||||
"mini-css-extract-plugin": "^0.7.0",
|
||||
"mocha": "^6.2.0",
|
||||
"moment-timezone": "^0.5.26",
|
||||
"mocha": "^6.2.2",
|
||||
"moment-timezone": "^0.5.27",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"ora": "^3.4.0",
|
||||
"photoswipe": "^4.1.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"postcss": "^7.0.18",
|
||||
"postcss": "^7.0.21",
|
||||
"postcss-browser-reporter": "^0.6.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
|
@ -83,17 +83,19 @@
|
|||
"postcss-url": "^8.0.0",
|
||||
"pubsub-js": "^1.7.0",
|
||||
"puppeteer-core": "^1.20.0",
|
||||
"resolve-url-loader": "^3.1.0",
|
||||
"resolve-url-loader": "^3.1.1",
|
||||
"sass-loader": "^7.3.1",
|
||||
"sinon": "^7.4.2",
|
||||
"sinon": "^7.5.0",
|
||||
"sockette": "^2.0.6",
|
||||
"style-loader": "^0.23.1",
|
||||
"sugarss": "^2.0.0",
|
||||
"svg-url-loader": "^2.3.3",
|
||||
"tar": "^4.4.10",
|
||||
"tar": "^4.4.13",
|
||||
"truncate": "^2.1.0",
|
||||
"url-loader": "^1.1.2",
|
||||
"vue": "^2.6.10",
|
||||
"vue-fullscreen": "^2.1.5",
|
||||
"vue-gettext": "^2.1.6",
|
||||
"vue-infinite-scroll": "^2.0.2",
|
||||
"vue-loader": "^14.2.4",
|
||||
"vue-luxon": "^0.7.0",
|
||||
|
@ -103,10 +105,10 @@
|
|||
"vue2-filters": "^0.6.1",
|
||||
"vue2-leaflet": "^2.2.1",
|
||||
"vuelidate": "^0.7.4",
|
||||
"vuetify": "^1.5.18",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-bundle-analyzer": "^3.5.0",
|
||||
"webpack-cli": "^3.3.8",
|
||||
"vuetify": "^1.5.21",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-md5-hash": "0.0.6",
|
||||
"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 Notify from "common/notify";
|
||||
import Config from "common/config";
|
||||
import Clipboard from "common/clipboard";
|
||||
import Components from "component/components";
|
||||
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 VueLuxon from "vue-luxon";
|
||||
import VueInfiniteScroll from "vue-infinite-scroll";
|
||||
import VueFullscreen from "vue-fullscreen";
|
||||
import VueFilters from "vue2-filters";
|
||||
import GetTextPlugin from "vue-gettext";
|
||||
import Maps from "maps/components";
|
||||
import PhotoPrism from "photoprism.vue";
|
||||
import Router from "vue-router";
|
||||
import Routes from "routes";
|
||||
import Session from "session";
|
||||
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
|
||||
const session = new Session(window.localStorage);
|
||||
const config = new Config(window.localStorage, window.appConfig);
|
||||
const viewer = new Viewer();
|
||||
const clipboard = new Clipboard(window.localStorage, "photo_clipboard");
|
||||
const isPublic = config.getValue("public");
|
||||
|
||||
// Assign helpers to VueJS prototype
|
||||
Vue.prototype.$event = Event;
|
||||
Vue.prototype.$alert = Alert;
|
||||
Vue.prototype.$notify = Notify;
|
||||
Vue.prototype.$viewer = viewer;
|
||||
Vue.prototype.$session = session;
|
||||
Vue.prototype.$session = Session;
|
||||
Vue.prototype.$api = Api;
|
||||
Vue.prototype.$socket = Socket;
|
||||
Vue.prototype.$config = config;
|
||||
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
|
||||
Vue.use(GetTextPlugin, {translations: Translations, silent: false, defaultLanguage: Vue.config.language});
|
||||
Vue.use(VueLuxon);
|
||||
Vue.use(VueInfiniteScroll);
|
||||
Vue.use(VueFullscreen);
|
||||
|
@ -68,6 +74,30 @@ const router = new Router({
|
|||
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
|
||||
/* eslint-disable no-unused-vars */
|
||||
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 Axios from "axios";
|
||||
import Notify from "common/notify";
|
||||
|
||||
const Api = axios.create({
|
||||
const Api = Axios.create({
|
||||
baseURL: "/api/v1",
|
||||
headers: {common: {
|
||||
"X-Session-Token": window.localStorage.getItem("session_token"),
|
||||
|
@ -11,7 +11,7 @@ const Api = axios.create({
|
|||
|
||||
Api.interceptors.request.use(function (config) {
|
||||
// Do something before request is sent
|
||||
Event.publish("ajax.start", config);
|
||||
Notify.ajaxStart();
|
||||
return config;
|
||||
}, function (error) {
|
||||
// Do something with request error
|
||||
|
@ -19,10 +19,15 @@ Api.interceptors.request.use(function (config) {
|
|||
});
|
||||
|
||||
Api.interceptors.response.use(function (response) {
|
||||
Event.publish("ajax.end", response);
|
||||
|
||||
Notify.ajaxEnd();
|
||||
return response;
|
||||
}, function (error) {
|
||||
Notify.ajaxEnd();
|
||||
|
||||
if (Axios.isCancel(error)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if(console && console.log) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@ -36,12 +41,7 @@ Api.interceptors.response.use(function (response) {
|
|||
errorMessage = data.message ? data.message : data.error;
|
||||
}
|
||||
|
||||
Event.publish("ajax.end");
|
||||
Event.publish("alert.error", errorMessage);
|
||||
|
||||
if(code === 401) {
|
||||
window.location = "/";
|
||||
}
|
||||
Notify.error(errorMessage);
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Api from "common/api";
|
||||
import Event from "pubsub-js";
|
||||
|
||||
class Config {
|
||||
/**
|
||||
|
@ -11,15 +11,14 @@ class Config {
|
|||
|
||||
this.values = values;
|
||||
|
||||
// this.setValues(JSON.parse(this.storage.getItem(this.storage_key)));
|
||||
// this.setValues(values);
|
||||
this.subscriptionId = Event.subscribe('config.updated', (ev, data) => this.setValues(data));
|
||||
}
|
||||
|
||||
setValues(values) {
|
||||
if(!values) return;
|
||||
if (!values) return;
|
||||
|
||||
for(let key in values) {
|
||||
if(values.hasOwnProperty(key)) {
|
||||
for (let key in values) {
|
||||
if (values.hasOwnProperty(key)) {
|
||||
this.setValue(key, values[key]);
|
||||
}
|
||||
}
|
||||
|
@ -52,14 +51,6 @@ class Config {
|
|||
|
||||
return this;
|
||||
}
|
||||
|
||||
pullFromServer() {
|
||||
return Api.get("config").then(
|
||||
(result) => {
|
||||
this.setValues(result.data);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 User from "model/user";
|
||||
import Api from "./api";
|
||||
import User from "../model/user";
|
||||
|
||||
class Session {
|
||||
export default class Session {
|
||||
/**
|
||||
* @param {Storage} storage
|
||||
*/
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
this.session_token = this.storage.getItem("session_token");
|
||||
this.auth = false;
|
||||
|
||||
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) {
|
||||
this.session_token = token;
|
||||
this.storage.setItem("session_token", token);
|
||||
Api.defaults.headers.common["X-Session-Token"] = token;
|
||||
return this.applyToken(token);
|
||||
}
|
||||
|
||||
getToken() {
|
||||
|
@ -27,13 +59,14 @@ class Session {
|
|||
deleteToken() {
|
||||
this.session_token = null;
|
||||
this.storage.removeItem("session_token");
|
||||
Api.defaults.headers.common["X-Session-Token"] = "";
|
||||
delete Api.defaults.headers.common["X-Session-Token"];
|
||||
this.deleteUser();
|
||||
}
|
||||
|
||||
setUser(user) {
|
||||
this.user = user;
|
||||
this.storage.setItem("user", JSON.stringify(user.getValues()));
|
||||
this.auth = true;
|
||||
}
|
||||
|
||||
getUser() {
|
||||
|
@ -42,15 +75,7 @@ class Session {
|
|||
|
||||
getEmail() {
|
||||
if (this.isUser()) {
|
||||
return this.user.userEmail;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
getFullName() {
|
||||
if (this.isUser()) {
|
||||
return this.user.userFirstName + " " + this.user.userLastName;
|
||||
return this.user.Email;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
@ -58,25 +83,34 @@ class Session {
|
|||
|
||||
getFirstName() {
|
||||
if (this.isUser()) {
|
||||
return this.user.userFirstName;
|
||||
return this.user.FirstName;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
getFullName() {
|
||||
if (this.isUser()) {
|
||||
return this.user.FirstName + " " + this.user.LastName;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
isUser() {
|
||||
return this.user.hasId();
|
||||
return this.user && this.user.hasId();
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return this.user.hasId() && this.user.userRole === "admin";
|
||||
return this.user && this.user.hasId() && this.user.Role === "admin";
|
||||
}
|
||||
|
||||
isAnonymous() {
|
||||
return !this.user.hasId();
|
||||
return !this.user || !this.user.hasId();
|
||||
}
|
||||
|
||||
deleteUser() {
|
||||
this.auth = false;
|
||||
this.user = null;
|
||||
this.storage.removeItem("user");
|
||||
}
|
||||
|
@ -84,7 +118,7 @@ class Session {
|
|||
login(email, password) {
|
||||
this.deleteToken();
|
||||
|
||||
return Api.post("session", { email: email, password: password }).then(
|
||||
return Api.post("session", {email: email, password: password}).then(
|
||||
(result) => {
|
||||
this.setToken(result.data.token);
|
||||
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 PLoadingBar from "./p-loading-bar.vue";
|
||||
import PPhotoDetails from "./p-photo-details.vue";
|
||||
|
@ -13,7 +13,7 @@ import PScrollTop from "./p-scroll-top.vue";
|
|||
const components = {};
|
||||
|
||||
components.install = (Vue) => {
|
||||
Vue.component("p-alert", PAlert);
|
||||
Vue.component("p-notify", PNotify);
|
||||
Vue.component("p-navigation", PNavigation);
|
||||
Vue.component("p-loading-bar", PLoadingBar);
|
||||
Vue.component("p-photo-details", PPhotoDetails);
|
||||
|
|
|
@ -57,10 +57,6 @@
|
|||
},
|
||||
|
||||
mounted () {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = 'p-busy-overlay';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
let stackSize = 0;
|
||||
|
||||
this.$event.subscribe('ajax.start', function () {
|
||||
|
@ -68,7 +64,6 @@
|
|||
|
||||
if(stackSize === 1) {
|
||||
this.start();
|
||||
document.getElementById('p-busy-overlay').style.display = 'block';
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
|
@ -77,7 +72,6 @@
|
|||
|
||||
if (stackSize === 0) {
|
||||
this.done();
|
||||
document.getElementById('p-busy-overlay').style.display = 'none';
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<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-title class="p-navigation-title">{{ $router.currentRoute.meta.area }}</v-toolbar-title>
|
||||
|
@ -58,13 +59,13 @@
|
|||
</v-list-tile-content>
|
||||
</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-title>Monochrome</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</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-title>Vibrant</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
|
@ -88,7 +89,7 @@
|
|||
</v-list-tile-content>
|
||||
</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-title>Work in progress...</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
|
@ -125,7 +126,7 @@
|
|||
</v-list-tile-content>
|
||||
</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-icon>date_range</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -133,9 +134,9 @@
|
|||
<v-list-tile-content>
|
||||
<v-list-tile-title>Events</v-list-tile-title>
|
||||
</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-icon>people</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -143,9 +144,9 @@
|
|||
<v-list-tile-content>
|
||||
<v-list-tile-title>People</v-list-tile-title>
|
||||
</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-icon>camera_roll</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -155,7 +156,7 @@
|
|||
</v-list-tile-content>
|
||||
</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-icon>settings</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
@ -164,6 +165,26 @@
|
|||
<v-list-tile-title>Settings</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</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-navigation-drawer>
|
||||
</div>
|
||||
|
@ -178,13 +199,18 @@
|
|||
return {
|
||||
drawer: null,
|
||||
mini: mini,
|
||||
session: this.$session,
|
||||
isPublic: this.$config.getValue("public"),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showNavigation: function () {
|
||||
this.drawer = true;
|
||||
this.mini = false;
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.$session.logout();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<v-snackbar
|
||||
id="p-alert"
|
||||
id="p-notify"
|
||||
v-model="visible"
|
||||
:color="color"
|
||||
:timeout="0"
|
||||
|
@ -23,7 +23,7 @@
|
|||
import Event from 'pubsub-js';
|
||||
|
||||
export default {
|
||||
name: 'p-alert',
|
||||
name: 'p-notify',
|
||||
data() {
|
||||
return {
|
||||
text: '',
|
||||
|
@ -37,30 +37,36 @@
|
|||
};
|
||||
},
|
||||
created() {
|
||||
this.subscriptionId = Event.subscribe('alert', this.handleAlertEvent);
|
||||
this.subscriptionId = Event.subscribe('notify', this.eventHandler);
|
||||
},
|
||||
destroyed() {
|
||||
Event.unsubscribe(this.subscriptionId);
|
||||
},
|
||||
methods: {
|
||||
handleAlertEvent: function (ev, message) {
|
||||
eventHandler: function (ev, data) {
|
||||
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) {
|
||||
case 'warning':
|
||||
this.addWarningMessage(message);
|
||||
this.addWarningMessage(m);
|
||||
break;
|
||||
case 'error':
|
||||
this.addErrorMessage(message);
|
||||
this.addErrorMessage(m);
|
||||
break;
|
||||
case 'success':
|
||||
this.addSuccessMessage(message);
|
||||
this.addSuccessMessage(m);
|
||||
break;
|
||||
case 'info':
|
||||
this.addInfoMessage(message);
|
||||
this.addInfoMessage(m);
|
||||
break;
|
||||
default:
|
||||
alert(message);
|
||||
alert(m);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -86,9 +92,15 @@
|
|||
this.lastMessageId++;
|
||||
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) {
|
||||
this.show();
|
|
@ -102,8 +102,8 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Event from "pubsub-js";
|
||||
import axios from "axios";
|
||||
import Api from "common/api";
|
||||
import Notify from "common/notify";
|
||||
|
||||
export default {
|
||||
name: 'p-photo-clipboard',
|
||||
|
@ -125,59 +125,44 @@
|
|||
this.expanded = false;
|
||||
},
|
||||
batchPrivate() {
|
||||
Event.publish("ajax.start");
|
||||
|
||||
const ctx = this;
|
||||
|
||||
axios.post("/api/v1/batch/photos/private", {"ids": this.selection}).then(function () {
|
||||
Event.publish("ajax.end");
|
||||
Event.publish("alert.success", "Toggled private flag");
|
||||
Api.post("batch/photos/private", {"ids": this.selection}).then(function () {
|
||||
Notify.success("Toggled private flag");
|
||||
ctx.clearClipboard();
|
||||
ctx.refresh();
|
||||
}).catch(() => {
|
||||
Event.publish("ajax.end");
|
||||
});
|
||||
},
|
||||
batchStory() {
|
||||
Event.publish("ajax.start");
|
||||
|
||||
const ctx = this;
|
||||
|
||||
axios.post("/api/v1/batch/photos/story", {"ids": this.selection}).then(function () {
|
||||
Event.publish("ajax.end");
|
||||
Event.publish("alert.success", "Toggled story flag");
|
||||
Api.post("batch/photos/story", {"ids": this.selection}).then(function () {
|
||||
Notify.success("Toggled story flag");
|
||||
ctx.clearClipboard();
|
||||
ctx.refresh();
|
||||
}).catch(() => {
|
||||
Event.publish("ajax.end");
|
||||
});
|
||||
},
|
||||
batchDelete() {
|
||||
this.dialog.delete = false;
|
||||
|
||||
Event.publish("ajax.start");
|
||||
|
||||
const ctx = this;
|
||||
|
||||
axios.post("/api/v1/batch/photos/delete", {"ids": this.selection}).then(function () {
|
||||
Event.publish("ajax.end");
|
||||
Event.publish("alert.success", "Photos deleted");
|
||||
Api.post("batch/photos/delete", {"ids": this.selection}).then(function () {
|
||||
Notify.success("Photos deleted");
|
||||
ctx.clearClipboard();
|
||||
ctx.refresh();
|
||||
}).catch(() => {
|
||||
Event.publish("ajax.end");
|
||||
});
|
||||
},
|
||||
batchTag() {
|
||||
this.$alert.warning("Not implemented yet");
|
||||
Notify.warning("Not implemented yet");
|
||||
this.expanded = false;
|
||||
},
|
||||
batchAlbum() {
|
||||
this.$alert.warning("Not implemented yet");
|
||||
Notify.warning("Not implemented yet");
|
||||
this.expanded = false;
|
||||
},
|
||||
batchDownload() {
|
||||
this.$alert.warning("Not implemented yet");
|
||||
Notify.warning("Not implemented yet");
|
||||
this.expanded = false;
|
||||
},
|
||||
openDocs() {
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
>
|
||||
<v-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')"
|
||||
aspect-ratio="1"
|
||||
class="grey lighten-2"
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
>
|
||||
<v-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')"
|
||||
aspect-ratio="1"
|
||||
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 {
|
||||
getEntityName() {
|
||||
return this.userFirstName + " " + this.userLastName;
|
||||
return this.FirstName + " " + this.LastName;
|
||||
}
|
||||
|
||||
getId() {
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
},
|
||||
openAlbum(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() {
|
||||
if (this.scrollDisabled) return;
|
||||
|
@ -147,7 +147,7 @@
|
|||
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||
|
||||
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);
|
||||
|
||||
if (this.scrollDisabled) {
|
||||
this.$alert.info(this.results.length + ' albums found');
|
||||
this.$notify.info(this.results.length + ' albums found');
|
||||
} else {
|
||||
this.$alert.info('More than 20 albums found');
|
||||
this.$notify.info('More than 20 albums found');
|
||||
|
||||
this.$nextTick(() => this.$emit("scrollRefresh"));
|
||||
}
|
||||
|
@ -222,7 +222,7 @@
|
|||
const album = new Album({"AlbumName": name});
|
||||
|
||||
album.save().then(() => {
|
||||
this.$alert.success(name + " created");
|
||||
this.$notify.success(name + " created");
|
||||
|
||||
this.filter.q = "";
|
||||
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
|
||||
},
|
||||
watch: {
|
||||
'$route' () {
|
||||
'$route'() {
|
||||
const query = this.$route.query;
|
||||
|
||||
this.filter.q = query['q'];
|
||||
|
@ -121,7 +121,7 @@
|
|||
},
|
||||
openLabel(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() {
|
||||
if (this.scrollDisabled) return;
|
||||
|
@ -143,7 +143,7 @@
|
|||
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||
|
||||
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);
|
||||
|
||||
if (this.scrollDisabled) {
|
||||
this.$alert.info(this.results.length + ' labels found');
|
||||
this.$notify.info(this.results.length + ' labels found');
|
||||
} else {
|
||||
this.$alert.info('More than 20 labels found');
|
||||
this.$notify.info('More than 20 labels found');
|
||||
|
||||
this.$nextTick(() => this.$emit("scrollRefresh"));
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<v-form ref="form" class="p-photo-import" lazy-validation @submit.prevent="submit" dense>
|
||||
<v-container fluid>
|
||||
<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>Press button to import photos from directory...</span>
|
||||
</p>
|
||||
|
@ -13,7 +14,7 @@
|
|||
<v-btn
|
||||
:disabled="busy"
|
||||
color="blue-grey"
|
||||
class="white--text ml-0"
|
||||
class="white--text ml-0 mt-2"
|
||||
depressed
|
||||
@click.stop="startImport()"
|
||||
>
|
||||
|
@ -26,7 +27,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Api from "common/api";
|
||||
import Axios from "axios";
|
||||
import Notify from "common/notify";
|
||||
import Event from "pubsub-js";
|
||||
|
||||
export default {
|
||||
|
@ -36,31 +39,75 @@
|
|||
started: false,
|
||||
busy: false,
|
||||
completed: 0,
|
||||
subscriptionId: '',
|
||||
fileName: '',
|
||||
source: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
console.log("SUBMIT");
|
||||
// DO NOTHING
|
||||
},
|
||||
startImport() {
|
||||
this.source = Axios.CancelToken.source();
|
||||
this.started = Date.now();
|
||||
this.busy = true;
|
||||
this.completed = 0;
|
||||
|
||||
this.$alert.info("Importing photos...");
|
||||
this.fileName = '';
|
||||
|
||||
const ctx = this;
|
||||
Notify.blockUI();
|
||||
|
||||
axios.post('/api/v1/import').then(function () {
|
||||
Event.publish("alert.success", "Import complete");
|
||||
Api.post('import', {}, { cancelToken: this.source.token }).then(function () {
|
||||
Notify.unblockUI();
|
||||
ctx.busy = false;
|
||||
ctx.completed = 100;
|
||||
}).catch(function () {
|
||||
Event.publish("alert.error", "Import failed");
|
||||
ctx.fileName = '';
|
||||
}).catch(function (e) {
|
||||
Notify.unblockUI();
|
||||
|
||||
if (Axios.isCancel(e)) {
|
||||
// run in background
|
||||
return
|
||||
}
|
||||
|
||||
Notify.error("Import failed");
|
||||
|
||||
ctx.busy = false;
|
||||
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>
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
|
||||
<v-container fluid>
|
||||
<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>Press button to re-index existing files and photos...</span>
|
||||
</p>
|
||||
|
@ -13,7 +14,7 @@
|
|||
<v-btn
|
||||
:disabled="busy"
|
||||
color="blue-grey"
|
||||
class="white--text ml-0"
|
||||
class="white--text ml-0 mt-2"
|
||||
depressed
|
||||
@click.stop="startIndexing()"
|
||||
>
|
||||
|
@ -26,7 +27,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Api from "common/api";
|
||||
import Axios from "axios";
|
||||
import Notify from "common/notify";
|
||||
import Event from "pubsub-js";
|
||||
|
||||
export default {
|
||||
|
@ -36,31 +39,75 @@
|
|||
started: false,
|
||||
busy: false,
|
||||
completed: 0,
|
||||
subscriptionId: '',
|
||||
fileName: '',
|
||||
source: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
console.log("SUBMIT");
|
||||
// DO NOTHING
|
||||
},
|
||||
startIndexing() {
|
||||
this.source = Axios.CancelToken.source();
|
||||
this.started = Date.now();
|
||||
this.busy = true;
|
||||
this.completed = 0;
|
||||
|
||||
this.$alert.info("Indexing photos...");
|
||||
this.fileName = '';
|
||||
|
||||
const ctx = this;
|
||||
Notify.blockUI();
|
||||
|
||||
axios.post('/api/v1/index').then(function () {
|
||||
Event.publish("alert.success", "Indexing complete");
|
||||
Api.post('index', {}, { cancelToken: this.source.token }).then(function () {
|
||||
Notify.unblockUI();
|
||||
ctx.busy = false;
|
||||
ctx.completed = 100;
|
||||
}).catch(function () {
|
||||
Event.publish("alert.error", "Indexing failed");
|
||||
ctx.fileName = '';
|
||||
}).catch(function (e) {
|
||||
Notify.unblockUI();
|
||||
|
||||
if (Axios.isCancel(e)) {
|
||||
// run in background
|
||||
return
|
||||
}
|
||||
|
||||
Notify.error("Indexing failed");
|
||||
|
||||
ctx.busy = false;
|
||||
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>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<v-btn
|
||||
:disabled="busy"
|
||||
color="blue-grey"
|
||||
class="white--text ml-0"
|
||||
class="white--text ml-0 mt-2"
|
||||
depressed
|
||||
@click.stop="uploadDialog()"
|
||||
>
|
||||
|
@ -29,8 +29,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Event from "pubsub-js";
|
||||
import Api from "common/api";
|
||||
import Notify from "common/notify";
|
||||
|
||||
export default {
|
||||
name: 'p-tab-upload',
|
||||
|
@ -48,7 +48,7 @@
|
|||
},
|
||||
methods: {
|
||||
submit() {
|
||||
console.log("SUBMIT");
|
||||
// DO NOTHING
|
||||
},
|
||||
uploadDialog() {
|
||||
this.$refs.upload.click();
|
||||
|
@ -67,9 +67,8 @@
|
|||
return
|
||||
}
|
||||
|
||||
this.$alert.info("Uploading photos...");
|
||||
|
||||
Event.publish("ajax.start");
|
||||
Notify.info("Uploading photos...");
|
||||
Notify.blockUI();
|
||||
|
||||
async function performUpload(ctx) {
|
||||
for (let i = 0; i < ctx.selected.length; i++) {
|
||||
|
@ -80,7 +79,7 @@
|
|||
|
||||
formData.append('files', file);
|
||||
|
||||
await axios.post('/api/v1/upload/' + ctx.started,
|
||||
await Api.post('upload/' + ctx.started,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
|
@ -90,7 +89,7 @@
|
|||
).then(function () {
|
||||
ctx.completed = Math.round((ctx.current / ctx.total) * 100);
|
||||
}).catch(function () {
|
||||
Event.publish("alert.error", "Upload failed");
|
||||
Notify.error("Upload failed");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -99,17 +98,17 @@
|
|||
this.indexing = true;
|
||||
const ctx = this;
|
||||
|
||||
axios.post('/api/v1/import/upload/' + this.started).then(function () {
|
||||
Event.publish("alert.success", "Upload complete");
|
||||
Api.post('import/upload/' + this.started).then(function () {
|
||||
Notify.unblockUI();
|
||||
Notify.success("Upload complete");
|
||||
ctx.busy = false;
|
||||
ctx.indexing = false;
|
||||
}).catch(function () {
|
||||
Event.publish("alert.error", "Failure while importing uploaded files");
|
||||
Notify.unblockUI();
|
||||
Notify.error("Failure while importing uploaded files");
|
||||
ctx.busy = 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];
|
||||
|
||||
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) {
|
||||
this.$router.push({name: 'Places', query: {q: photo.LocName}});
|
||||
this.$router.push({name: "places", query: {q: photo.LocName}});
|
||||
} 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) {
|
||||
this.$router.push({name: 'Places', query: {q: photo.LocCountry}});
|
||||
this.$router.push({name: "places", query: {q: photo.LocCountry}});
|
||||
} else {
|
||||
this.$router.push({name: 'Places', query: {q: photo.CountryName}});
|
||||
this.$router.push({name: "places", query: {q: photo.CountryName}});
|
||||
}
|
||||
},
|
||||
openPhoto(index) {
|
||||
|
@ -120,7 +120,7 @@
|
|||
this.scrollDisabled = (response.models.length < this.pageSize);
|
||||
|
||||
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);
|
||||
|
||||
if (this.scrollDisabled) {
|
||||
this.$alert.info(this.results.length + ' photos found');
|
||||
this.$notify.info(this.results.length + ' photos found');
|
||||
} else {
|
||||
this.$alert.info('More than 50 photos found');
|
||||
this.$notify.info('More than 50 photos found');
|
||||
|
||||
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();
|
||||
},
|
||||
currentPositionError(error) {
|
||||
this.$alert.warning(error.message);
|
||||
this.$notify.warning(error.message);
|
||||
},
|
||||
currentPosition() {
|
||||
if ("geolocation" in navigator) {
|
||||
this.$alert.success('Finding your position...');
|
||||
this.$notify.success('Finding your position...');
|
||||
navigator.geolocation.getCurrentPosition(this.currentPositionSuccess, this.currentPositionError);
|
||||
} else {
|
||||
this.$alert.warning('Geolocation is not available');
|
||||
this.$notify.warning('Geolocation is not available');
|
||||
}
|
||||
},
|
||||
formChange() {
|
||||
|
@ -156,7 +156,7 @@
|
|||
}
|
||||
|
||||
if (photos.length === 0) {
|
||||
this.$alert.warning('No locations found');
|
||||
this.$notify.warning('No locations found');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -169,13 +169,13 @@
|
|||
});
|
||||
|
||||
if (photos.length > 100) {
|
||||
this.$alert.info('More than 100 photos found');
|
||||
this.$notify.info('More than 100 photos found');
|
||||
} else {
|
||||
this.$alert.info(photos.length + ' photos found');
|
||||
this.$notify.info(photos.length + ' photos found');
|
||||
}
|
||||
},
|
||||
updateQuery() {
|
||||
this.$router.replace({query: this.query});
|
||||
this.$router.replace({query: this.query}).catch(err => {});
|
||||
|
||||
if(this.query.lat && this.query.long) {
|
||||
this.position = L.latLng(this.query.lat, this.query.long);
|
||||
|
@ -194,7 +194,7 @@
|
|||
|
||||
this.updateQuery();
|
||||
|
||||
this.$router.replace({query: this.query});
|
||||
this.$router.replace({query: this.query}).catch(err => {});
|
||||
|
||||
const params = {
|
||||
count: this.pageSize,
|
||||
|
@ -206,7 +206,7 @@
|
|||
|
||||
Photo.search(params).then(response => {
|
||||
if (!response.models.length) {
|
||||
this.$alert.warning('No photos found');
|
||||
this.$notify.warning('No photos found');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,29 +1,38 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar flat color="blue-grey lighten-4">
|
||||
<v-toolbar-title>Not implemented yet</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<p>
|
||||
Issues labeled <a href="https://github.com/photoprism/photoprism/labels/help%20wanted">help wanted</a> /
|
||||
<a href="https://github.com/photoprism/photoprism/labels/easy">easy</a> can be good (first)
|
||||
contributions.
|
||||
Our <a href="https://github.com/photoprism/photoprism/wiki">Developer Guide</a> contains all information
|
||||
necessary to get you started.
|
||||
</p>
|
||||
</v-container>
|
||||
<div class="p-page p-page-settings">
|
||||
<v-tabs
|
||||
v-model="active"
|
||||
flat
|
||||
grow
|
||||
color="blue-grey lighten-4"
|
||||
slider-color="blue-grey darken-1"
|
||||
height="64"
|
||||
>
|
||||
<v-tab id="tab-upload">
|
||||
General
|
||||
</v-tab>
|
||||
<v-tab-item>
|
||||
<p-tab-general></p-tab-general>
|
||||
</v-tab-item>
|
||||
</v-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tabGeneral from "pages/settings/general.vue";
|
||||
|
||||
export default {
|
||||
name: 'todo',
|
||||
data() {
|
||||
return {};
|
||||
name: 'p-page-settings',
|
||||
components: {
|
||||
'p-tab-general': tabGeneral,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
active: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
methods: {}
|
||||
};
|
||||
</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">
|
||||
<p-loading-bar height="4"></p-loading-bar>
|
||||
|
||||
<p-alert></p-alert>
|
||||
<p-notify></p-notify>
|
||||
|
||||
<v-app>
|
||||
<p-navigation></p-navigation>
|
||||
|
@ -22,14 +22,6 @@
|
|||
export default {
|
||||
name: 'photoprism',
|
||||
computed: {},
|
||||
methods: {
|
||||
login() {
|
||||
// this.$refs.loginDialog.open();
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.$session.logout();
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -7,80 +7,87 @@ import People from "pages/people.vue";
|
|||
import Library from "pages/library.vue";
|
||||
import Share from "pages/share.vue";
|
||||
import Settings from "pages/settings.vue";
|
||||
import Login from "pages/login.vue";
|
||||
import Todo from "pages/todo.vue";
|
||||
|
||||
export default [
|
||||
{
|
||||
name: "Home",
|
||||
name: "home",
|
||||
path: "/",
|
||||
redirect: "/photos",
|
||||
},
|
||||
{
|
||||
name: "Photos",
|
||||
name: "login",
|
||||
path: "/login",
|
||||
component: Login,
|
||||
meta: {area: "Login"},
|
||||
},
|
||||
{
|
||||
name: "photos",
|
||||
path: "/photos",
|
||||
component: Photos,
|
||||
meta: {area: "Photos"},
|
||||
},
|
||||
{
|
||||
name: "Albums",
|
||||
name: "albums",
|
||||
path: "/albums",
|
||||
component: Albums,
|
||||
meta: {area: "Albums"},
|
||||
},
|
||||
{
|
||||
name: "Favorites",
|
||||
name: "favorites",
|
||||
path: "/favorites",
|
||||
component: Photos,
|
||||
meta: {area: "Favorites"},
|
||||
props: {staticFilter: {favorites: true}},
|
||||
},
|
||||
{
|
||||
name: "Places",
|
||||
name: "places",
|
||||
path: "/places",
|
||||
component: Places,
|
||||
meta: {area: "Places"},
|
||||
},
|
||||
{
|
||||
name: "Labels",
|
||||
name: "labels",
|
||||
path: "/labels",
|
||||
component: Labels,
|
||||
meta: {area: "Labels"},
|
||||
},
|
||||
{
|
||||
name: "Events",
|
||||
name: "events",
|
||||
path: "/events",
|
||||
component: Events,
|
||||
meta: {area: "Events"},
|
||||
},
|
||||
{
|
||||
name: "People",
|
||||
name: "people",
|
||||
path: "/people",
|
||||
component: People,
|
||||
meta: {area: "People"},
|
||||
},
|
||||
{
|
||||
name: "Filters",
|
||||
name: "filters",
|
||||
path: "/filters",
|
||||
component: Todo,
|
||||
meta: {area: "Filters"},
|
||||
},
|
||||
{
|
||||
name: "Library",
|
||||
name: "library",
|
||||
path: "/library",
|
||||
component: Library,
|
||||
meta: {area: "Library"},
|
||||
meta: {area: "Library", auth: true},
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
name: "share",
|
||||
path: "/share",
|
||||
component: Share,
|
||||
meta: {area: "Share"},
|
||||
meta: {area: "Share", auth: true},
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
name: "settings",
|
||||
path: "/settings",
|
||||
component: Settings,
|
||||
meta: {area: "Settings"},
|
||||
meta: {area: "Settings", auth: true},
|
||||
},
|
||||
{
|
||||
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
|
||||
.click('a[href="/labels"]')
|
||||
.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");
|
||||
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,32 +1,32 @@
|
|||
import Alert from "common/alert";
|
||||
import Notify from "common/notify";
|
||||
let sinon = require("sinon");
|
||||
|
||||
describe("common/alert", () => {
|
||||
it("should call alert.info", () => {
|
||||
let spy = sinon.spy(Alert, "info");
|
||||
Alert.info("message");
|
||||
let spy = sinon.spy(Notify, "info");
|
||||
Notify.info("message");
|
||||
sinon.assert.calledOnce(spy);
|
||||
spy.resetHistory();
|
||||
});
|
||||
|
||||
it("should call alert.warning", () => {
|
||||
let spy = sinon.spy(Alert, "warning");
|
||||
Alert.warning("message");
|
||||
let spy = sinon.spy(Notify, "warning");
|
||||
Notify.warning("message");
|
||||
sinon.assert.calledOnce(spy);
|
||||
spy.resetHistory();
|
||||
});
|
||||
|
||||
it("should call alert.error", () => {
|
||||
let spy = sinon.spy(Alert, "error");
|
||||
Alert.error("message");
|
||||
let spy = sinon.spy(Notify, "error");
|
||||
Notify.error("message");
|
||||
sinon.assert.calledOnce(spy);
|
||||
spy.resetHistory();
|
||||
});
|
||||
|
||||
it("should call alert.success", () => {
|
||||
let spy = sinon.spy(Alert, "success");
|
||||
Alert.success("message");
|
||||
let spy = sinon.spy(Notify, "success");
|
||||
Notify.success("message");
|
||||
sinon.assert.calledOnce(spy);
|
||||
spy.resetHistory();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -35,15 +35,15 @@ describe('common/session', () => {
|
|||
it('should set, get and delete user', () => {
|
||||
const storage = window.localStorage;
|
||||
const session = new Session(storage);
|
||||
assert.equal(session.user.ID, undefined);
|
||||
const values = {ID: 5, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
||||
assert.equal(session.user, null);
|
||||
const values = {ID: 5, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||
const user = new User(values);
|
||||
session.setUser(user);
|
||||
assert.equal(session.user.userFirstName, "Max");
|
||||
assert.equal(session.user.userRole, "admin");
|
||||
assert.equal(session.user.FirstName, "Max");
|
||||
assert.equal(session.user.Role, "admin");
|
||||
const result = session.getUser();
|
||||
assert.equal(result.ID, 5);
|
||||
assert.equal(result.userEmail, "test@test.com");
|
||||
assert.equal(result.Email, "test@test.com");
|
||||
session.deleteUser();
|
||||
assert.equal(session.user, null);
|
||||
});
|
||||
|
@ -51,12 +51,12 @@ describe('common/session', () => {
|
|||
it('should get user email', () => {
|
||||
const storage = window.localStorage;
|
||||
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);
|
||||
session.setUser(user);
|
||||
const result = session.getEmail();
|
||||
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);
|
||||
session.setUser(user2);
|
||||
const result2 = session.getEmail();
|
||||
|
@ -67,12 +67,12 @@ describe('common/session', () => {
|
|||
it('should get user firstname', () => {
|
||||
const storage = window.localStorage;
|
||||
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);
|
||||
session.setUser(user);
|
||||
const result = session.getFirstName();
|
||||
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);
|
||||
session.setUser(user2);
|
||||
const result2 = session.getFirstName();
|
||||
|
@ -83,12 +83,12 @@ describe('common/session', () => {
|
|||
it('should get user full name', () => {
|
||||
const storage = window.localStorage;
|
||||
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);
|
||||
session.setUser(user);
|
||||
const result = session.getFullName();
|
||||
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);
|
||||
session.setUser(user2);
|
||||
const result2 = session.getFullName();
|
||||
|
@ -99,7 +99,7 @@ describe('common/session', () => {
|
|||
it('should test whether user is set', () => {
|
||||
const storage = window.localStorage;
|
||||
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);
|
||||
session.setUser(user);
|
||||
const result = session.isUser();
|
||||
|
@ -110,7 +110,7 @@ describe('common/session', () => {
|
|||
it('should test whether user is admin', () => {
|
||||
const storage = window.localStorage;
|
||||
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);
|
||||
session.setUser(user);
|
||||
const result = session.isAdmin();
|
||||
|
@ -121,7 +121,7 @@ describe('common/session', () => {
|
|||
it('should test whether user is anonymous', () => {
|
||||
const storage = window.localStorage;
|
||||
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);
|
||||
session.setUser(user);
|
||||
const result = session.isAnonymous();
|
||||
|
@ -131,7 +131,7 @@ describe('common/session', () => {
|
|||
|
||||
it('should test login and logout', async() => {
|
||||
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);
|
||||
const storage = window.localStorage;
|
||||
const session = new Session(storage);
|
||||
|
@ -139,10 +139,10 @@ describe('common/session', () => {
|
|||
assert.equal(session.storage.user, undefined);
|
||||
await session.login("test@test.com", "passwd");
|
||||
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();
|
||||
assert.equal(session.session_token, null);
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,14 +9,14 @@ describe("model/user", () => {
|
|||
const mock = new MockAdapter(Api);
|
||||
|
||||
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 result = user.getEntityName();
|
||||
assert.equal(result, "Max Last");
|
||||
});
|
||||
|
||||
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 result = user.getId();
|
||||
assert.equal(result, 5);
|
||||
|
@ -34,7 +34,7 @@ describe("model/user", () => {
|
|||
|
||||
it("should get register form", async() => {
|
||||
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 result = await user.getRegisterForm();
|
||||
assert.equal(result.definition, "registerForm");
|
||||
|
@ -43,7 +43,7 @@ describe("model/user", () => {
|
|||
|
||||
it("should get profile form", async() => {
|
||||
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 result = await user.getProfileForm();
|
||||
assert.equal(result.definition, "profileForm");
|
||||
|
@ -52,20 +52,20 @@ describe("model/user", () => {
|
|||
|
||||
it("should get change password", async() => {
|
||||
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 result = await user.changePassword("old", "new");
|
||||
assert.equal(result.new_password, "new");
|
||||
});
|
||||
|
||||
it("should save profile", async() => {
|
||||
mock.onPost("users/55/profile").reply(200, {userFirstName: "MaxNew", userLastName: "LastNew"});
|
||||
const values = {ID: 55, userFirstName: "Max", userLastName: "Last", userEmail: "test@test.com", userRole: "admin"};
|
||||
mock.onPost("users/55/profile").reply(200, {FirstName: "MaxNew", LastName: "LastNew"});
|
||||
const values = {ID: 55, FirstName: "Max", LastName: "Last", Email: "test@test.com", Role: "admin"};
|
||||
const user = new User(values);
|
||||
assert.equal(user.userFirstName, "Max");
|
||||
assert.equal(user.userLastName, "Last");
|
||||
assert.equal(user.FirstName, "Max");
|
||||
assert.equal(user.LastName, "Last");
|
||||
await user.saveProfile();
|
||||
assert.equal(user.userFirstName, "MaxNew");
|
||||
assert.equal(user.userLastName, "LastNew");
|
||||
assert.equal(user.FirstName, "MaxNew");
|
||||
assert.equal(user.LastName, "LastNew");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -199,7 +199,7 @@ const config = {
|
|||
// No sourcemap for production
|
||||
if (isDev) {
|
||||
const devToolPlugin = new webpack.SourceMapDevToolPlugin({
|
||||
filename: "[name].map",
|
||||
filename: "[file].map",
|
||||
});
|
||||
|
||||
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-logging v0.0.0-20190409182557-13b4fff49234 // 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/go-errors/errors v1.0.1 // 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/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 // 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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // 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/mattn/go-isatty v0.0.4 // 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/myesui/uuid v1.0.0 // indirect
|
||||
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/goleveldb v0.0.0-20171020122428-b9ff6c35079e // indirect
|
||||
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/jaeger-client-go v2.15.0+incompatible // 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/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/image v0.0.0-20181116024801-cd38e8056d9b // indirect
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
||||
golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 // indirect
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // 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/validator.v8 v8.18.2 // 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/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/djherbis/times v1.1.0 h1:NFhBDODme0XNX+/5ETW9qL6v3Ty57psiXIQBrzzg44E=
|
||||
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/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c=
|
||||
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/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
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.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
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/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
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.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
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/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-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||
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/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/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=
|
||||
|
@ -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 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/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
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/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
|
||||
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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
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/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
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/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
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/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=
|
||||
|
@ -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/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
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/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/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
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/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
|
||||
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/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.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-20181210145518-4c664cb3ad2f h1:+feYJlxPM00jEkdybexHiwIIOVuClwTEbh1WLiNr0mk=
|
||||
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.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.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/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
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-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-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-20180905080454-ebe1bf3edb33/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-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-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg=
|
||||
golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||
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/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-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-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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
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/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GET /api/v1/albums
|
||||
|
@ -36,8 +34,8 @@ func GetAlbums(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
||||
c.Header("X-Result-Count", strconv.Itoa(form.Count))
|
||||
c.Header("X-Result-Offset", strconv.Itoa(form.Offset))
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
|
@ -50,6 +48,11 @@ type CreateAlbumParams struct {
|
|||
// POST /api/v1/albums
|
||||
func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/albums", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var params CreateAlbumParams
|
||||
|
||||
if err := c.BindJSON(¶ms); err != nil {
|
||||
|
@ -75,6 +78,11 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
// uuid: string Album UUID
|
||||
func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
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())
|
||||
|
||||
album, err := search.FindAlbumByUUID(c.Param("uuid"))
|
||||
|
@ -97,6 +105,11 @@ func LikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
// uuid: string Album UUID
|
||||
func DislikeAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||
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())
|
||||
|
||||
album, err := search.FindAlbumByUUID(c.Param("uuid"))
|
||||
|
|
|
@ -8,24 +8,24 @@ import (
|
|||
|
||||
func TestGetAlbums(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
GetAlbums(router, ctx)
|
||||
app, router, conf := NewApiTest()
|
||||
GetAlbums(router, conf)
|
||||
result := PerformRequest(app, "GET", "/api/v1/albums?count=10")
|
||||
|
||||
assert.Equal(t, http.StatusOK, result.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
GetAlbums(router, ctx)
|
||||
app, router, conf := NewApiTest()
|
||||
GetAlbums(router, conf)
|
||||
result := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
||||
t.Log(result.Body)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, result.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
app, router, conf := NewApiTest()
|
||||
t.Log(router)
|
||||
t.Log(ctx)
|
||||
t.Log(conf)
|
||||
result := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
||||
t.Log(result.Body)
|
||||
|
||||
|
@ -47,9 +47,9 @@ func TestLikeAlbum(t *testing.T) {
|
|||
|
||||
func TestDislikeAlbum(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")
|
||||
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/models"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
@ -21,6 +20,11 @@ type BatchParams struct {
|
|||
// POST /api/v1/batch/photos/delete
|
||||
func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/batch/photos/delete", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
var params BatchParams
|
||||
|
@ -52,6 +56,11 @@ func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) {
|
|||
// POST /api/v1/batch/photos/private
|
||||
func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/batch/photos/private", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
var params BatchParams
|
||||
|
@ -82,6 +91,11 @@ func BatchPhotosPrivate(router *gin.RouterGroup, conf *config.Config) {
|
|||
// POST /api/v1/batch/photos/story
|
||||
func BatchPhotosStory(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/batch/photos/story", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
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{}{
|
||||
"photo_story": gorm.Expr("IF (`photo_story`, 0, 1)"),
|
||||
"photo_private": "0",
|
||||
})
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
|
|
|
@ -7,5 +7,6 @@ import (
|
|||
)
|
||||
|
||||
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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
|
@ -34,6 +35,11 @@ func Import(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
path := conf.ImportPath()
|
||||
|
||||
|
@ -42,14 +48,19 @@ func Import(router *gin.RouterGroup, conf *config.Config) {
|
|||
path = path + subPath
|
||||
}
|
||||
|
||||
log.Infof("importing photos from %s", path)
|
||||
event.Info(fmt.Sprintf("importing photos from \"%s\"", filepath.Base(path)))
|
||||
|
||||
initImporter(conf)
|
||||
|
||||
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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
)
|
||||
|
||||
|
@ -27,17 +27,26 @@ func initIndexer(conf *config.Config) {
|
|||
// POST /api/v1/index
|
||||
func Index(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/index", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
path := conf.OriginalsPath()
|
||||
|
||||
log.Infof("indexing photos in %s", path)
|
||||
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))
|
||||
|
||||
initIndexer(conf)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
||||
c.Header("X-Result-Count", strconv.Itoa(form.Count))
|
||||
c.Header("X-Result-Offset", strconv.Itoa(form.Offset))
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
|
@ -44,6 +44,11 @@ func GetLabels(router *gin.RouterGroup, conf *config.Config) {
|
|||
// slug: string Label slug name
|
||||
func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
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())
|
||||
|
||||
label, err := search.FindLabelBySlug(c.Param("slug"))
|
||||
|
@ -66,6 +71,11 @@ func LikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
|||
// slug: string Label slug name
|
||||
func DislikeLabel(router *gin.RouterGroup, conf *config.Config) {
|
||||
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())
|
||||
|
||||
label, err := search.FindLabelBySlug(c.Param("slug"))
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
|
@ -46,8 +45,8 @@ func GetPhotos(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
||||
c.Header("X-Result-Count", strconv.Itoa(form.Count))
|
||||
c.Header("X-Result-Offset", strconv.Itoa(form.Offset))
|
||||
|
||||
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
|
||||
func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
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())
|
||||
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
|
||||
func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||
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())
|
||||
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/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
@ -22,6 +21,11 @@ func Upload(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
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("debug %t\n", conf.Debug())
|
||||
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-filename %s\n", conf.LogFilename())
|
||||
fmt.Printf("pid-filename %s\n", conf.PIDFilename())
|
||||
|
|
|
@ -17,10 +17,12 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
tensorflow "github.com/tensorflow/tensorflow/tensorflow/go"
|
||||
"github.com/urfave/cli"
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
db *gorm.DB
|
||||
cache *gc.Cache
|
||||
config *Params
|
||||
}
|
||||
|
||||
|
@ -201,11 +203,25 @@ func (c *Config) Debug() bool {
|
|||
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.
|
||||
func (c *Config) ReadOnly() bool {
|
||||
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.
|
||||
func (c *Config) LogLevel() log.Level {
|
||||
if c.Debug() {
|
||||
|
@ -224,6 +240,11 @@ func (c *Config) ConfigFile() string {
|
|||
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.
|
||||
func (c *Config) ConfigPath() string {
|
||||
if c.config.ConfigPath == "" {
|
||||
|
@ -431,6 +452,15 @@ func (c *Config) HttpStaticBuildPath() string {
|
|||
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.
|
||||
func (c *Config) Db() *gorm.DB {
|
||||
if c.db == nil {
|
||||
|
@ -501,11 +531,13 @@ func (c *Config) ClientConfig() ClientConfig {
|
|||
"copyright": c.Copyright(),
|
||||
"debug": c.Debug(),
|
||||
"readonly": c.ReadOnly(),
|
||||
"public": c.Public(),
|
||||
"cameras": cameras,
|
||||
"countries": countries,
|
||||
"thumbnails": Thumbnails,
|
||||
"jsHash": jsHash,
|
||||
"cssHash": cssHash,
|
||||
"settings": c.Settings(),
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -516,6 +548,7 @@ func (c *Config) Init(ctx context.Context) error {
|
|||
return c.connectToDatabase(ctx)
|
||||
}
|
||||
|
||||
// Shutdown closes open database connections.
|
||||
func (c *Config) Shutdown() {
|
||||
if err := c.CloseDb(); err != nil {
|
||||
log.Errorf("could not close database connection: %s", err)
|
||||
|
@ -523,3 +556,15 @@ func (c *Config) Shutdown() {
|
|||
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 (
|
||||
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",
|
||||
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{
|
||||
Name: "log-level, l",
|
||||
Usage: "trace, debug, info, warning, error, fatal or panic",
|
||||
|
@ -132,11 +143,6 @@ var GlobalFlags = []cli.Flag{
|
|||
Usage: "debug, release or test",
|
||||
EnvVar: "PHOTOPRISM_HTTP_MODE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "http-password",
|
||||
Usage: "HTTP server password (optional)",
|
||||
EnvVar: "PHOTOPRISM_HTTP_PASSWORD",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "sql-port, s",
|
||||
Usage: "built-in SQL server port",
|
||||
|
|
|
@ -33,6 +33,8 @@ type Params struct {
|
|||
Copyright string
|
||||
Debug bool `yaml:"debug" flag:"debug"`
|
||||
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"`
|
||||
ConfigFile string
|
||||
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)
|
||||
|
||||
c := &Params{
|
||||
Public: true,
|
||||
ReadOnly: false,
|
||||
DarktableBin: "/usr/bin/darktable-cli",
|
||||
AssetsPath: assetsPath,
|
||||
CachePath: testDataPath + "/cache",
|
||||
|
@ -40,7 +42,7 @@ func NewTestParams() *Params {
|
|||
ImportPath: testDataPath + "/import",
|
||||
ExportPath: testDataPath + "/export",
|
||||
DatabaseDriver: "mysql",
|
||||
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-mysql:4001)/photoprism?parseTime=true",
|
||||
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-db:4001)/photoprism?parseTime=true",
|
||||
}
|
||||
|
||||
return c
|
||||
|
@ -59,7 +61,7 @@ func NewTestParamsError() *Params {
|
|||
ImportPath: testDataPath + "/import",
|
||||
ExportPath: testDataPath + "/export",
|
||||
DatabaseDriver: "mysql",
|
||||
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-mysql:4001)/photoprism?parseTime=true",
|
||||
DatabaseDsn: "photoprism:photoprism@tcp(photoprism-db:4001)/photoprism?parseTime=true",
|
||||
}
|
||||
|
||||
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
|
||||
PhotoFocalLength int
|
||||
PhotoIso int
|
||||
PhotoAperture float64
|
||||
PhotoFNumber float64
|
||||
PhotoExposure string
|
||||
PhotoViews uint
|
||||
Camera *Camera
|
||||
|
|
|
@ -21,11 +21,14 @@ type Exif struct {
|
|||
Artist string
|
||||
CameraMake string
|
||||
CameraModel string
|
||||
Description string
|
||||
LensMake string
|
||||
LensModel string
|
||||
Flash bool
|
||||
FocalLength int
|
||||
Exposure string
|
||||
Aperture float64
|
||||
FNumber float64
|
||||
Iso int
|
||||
Lat float64
|
||||
Long float64
|
||||
|
@ -148,6 +151,17 @@ func (m *MediaFile) Exif() (result *Exif, err error) {
|
|||
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 {
|
||||
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
|
||||
|
||||
return m.exifData, nil
|
||||
|
|
|
@ -11,20 +11,75 @@ import (
|
|||
func TestMediaFile_Exif_JPEG(t *testing.T) {
|
||||
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, "Africa/Johannesburg", info.TimeZone)
|
||||
t.Logf("UTC: %s", info.TakenAt.String())
|
||||
t.Logf("Local: %s", info.TakenAtLocal.String())
|
||||
assert.Equal(t, "", info.UUID)
|
||||
assert.Equal(t, "2013-11-26 13:53:55 +0000 UTC", info.TakenAt.String())
|
||||
assert.Equal(t, "2013-11-26 15:53:55 +0000 UTC", 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) {
|
||||
|
@ -44,7 +99,25 @@ func TestMediaFile_Exif_DNG(t *testing.T) {
|
|||
|
||||
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", 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) {
|
||||
|
@ -76,7 +149,27 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
|
|||
|
||||
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, "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 {
|
||||
t.Error(err)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
@ -78,11 +79,16 @@ func (i *Importer) ImportPhotosFromDirectory(importPath string) {
|
|||
relatedFiles, mainFile, err := mediaFile.RelatedFiles()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
event.Publish("import.file", event.Data{
|
||||
"fileName": mainFile.Filename(),
|
||||
"baseName": filepath.Base(mainFile.Filename()),
|
||||
})
|
||||
|
||||
for _, relatedMediaFile := range relatedFiles {
|
||||
relativeFilename := relatedMediaFile.RelativeFilename(importPath)
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
)
|
||||
|
@ -113,6 +114,12 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
|||
fileName := mediaFile.RelativeFilename(i.originalsPath())
|
||||
fileHash := mediaFile.Hash()
|
||||
|
||||
event.Publish("index.file", event.Data{
|
||||
"fileHash": fileHash,
|
||||
"fileName": fileName,
|
||||
"baseName": filepath.Base(fileName),
|
||||
})
|
||||
|
||||
exifData, err := mediaFile.Exif()
|
||||
|
||||
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.Lens = models.NewLens(mediaFile.LensModel(), mediaFile.LensMake()).FirstOrCreate(i.db)
|
||||
photo.PhotoFocalLength = mediaFile.FocalLength()
|
||||
photo.PhotoAperture = mediaFile.Aperture()
|
||||
photo.PhotoFNumber = mediaFile.FNumber()
|
||||
photo.PhotoIso = mediaFile.Iso()
|
||||
photo.PhotoExposure = mediaFile.Exposure()
|
||||
}
|
||||
|
|
|
@ -156,14 +156,14 @@ func (m *MediaFile) FocalLength() int {
|
|||
return result
|
||||
}
|
||||
|
||||
// Aperture returns the aperture with which the media file was created.
|
||||
func (m *MediaFile) Aperture() float64 {
|
||||
// FNumber returns the F number with which the media file was created.
|
||||
func (m *MediaFile) FNumber() float64 {
|
||||
info, err := m.Exif()
|
||||
|
||||
var result float64
|
||||
|
||||
if err == nil {
|
||||
result = info.Aperture
|
||||
result = info.FNumber
|
||||
}
|
||||
|
||||
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) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/cat_brown.jpg")
|
||||
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) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
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 {
|
||||
q = q.Where("photos.photo_aperture >= ?", form.Fmin)
|
||||
q = q.Where("photos.photo_f_number >= ?", form.Fmin)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -20,12 +20,17 @@ type PhotoSearchResult struct {
|
|||
PhotoKeywords string
|
||||
PhotoColors string
|
||||
PhotoColor string
|
||||
PhotoLat float64
|
||||
PhotoLong float64
|
||||
PhotoFavorite bool
|
||||
PhotoPrivate bool
|
||||
PhotoSensitive bool
|
||||
PhotoStory bool
|
||||
PhotoLat float64
|
||||
PhotoLong float64
|
||||
PhotoAltitude int
|
||||
PhotoFocalLength int
|
||||
PhotoIso int
|
||||
PhotoFNumber float64
|
||||
PhotoExposure string
|
||||
|
||||
// Camera
|
||||
CameraID uint
|
||||
|
|
|
@ -15,9 +15,18 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
// Static assets like js and css files
|
||||
router.Static("/static", conf.HttpStaticPath())
|
||||
|
||||
// socket.io
|
||||
/* s := router.Group("/socket.io")
|
||||
{
|
||||
api.Socket(s, conf)
|
||||
} */
|
||||
|
||||
// JSON-REST API Version 1
|
||||
v1 := router.Group("/api/v1")
|
||||
{
|
||||
api.CreateSession(v1, conf)
|
||||
api.DeleteSession(v1, conf)
|
||||
|
||||
api.GetThumbnail(v1, conf)
|
||||
api.GetDownload(v1, conf)
|
||||
|
||||
|
@ -43,6 +52,11 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.DislikeAlbum(v1, conf)
|
||||
api.AlbumThumbnail(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)
|
||||
|
|
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