forked from platypush/platypush
Migrated ImageCarousel widget
This commit is contained in:
parent
243e56b194
commit
cc3e52c69d
10 changed files with 353 additions and 36 deletions
|
@ -4,7 +4,7 @@ import re
|
||||||
from flask import Blueprint, abort, send_from_directory
|
from flask import Blueprint, abort, send_from_directory
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.backend.http.app import template_folder, static_folder
|
from platypush.backend.http.app import template_folder
|
||||||
|
|
||||||
|
|
||||||
img_folder = os.path.join(template_folder, 'img')
|
img_folder = os.path.join(template_folder, 'img')
|
||||||
|
@ -24,7 +24,6 @@ __routes__ = [
|
||||||
def resources_path(path):
|
def resources_path(path):
|
||||||
""" Custom static resources """
|
""" Custom static resources """
|
||||||
path_tokens = path.split('/')
|
path_tokens = path.split('/')
|
||||||
filename = path_tokens.pop(-1)
|
|
||||||
http_conf = Config.get('backend.http')
|
http_conf = Config.get('backend.http')
|
||||||
resource_dirs = http_conf.get('resource_dirs', {})
|
resource_dirs = http_conf.get('resource_dirs', {})
|
||||||
|
|
||||||
|
@ -61,6 +60,7 @@ def serve_favicon():
|
||||||
""" favicon.ico icon """
|
""" favicon.ico icon """
|
||||||
return send_from_directory(template_folder, 'favicon.ico')
|
return send_from_directory(template_folder, 'favicon.ico')
|
||||||
|
|
||||||
|
|
||||||
@img.route('/img/<path:path>', methods=['GET'])
|
@img.route('/img/<path:path>', methods=['GET'])
|
||||||
def imgpath(path):
|
def imgpath(path):
|
||||||
""" Default static images """
|
""" Default static images """
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.date-time {
|
.date-time {
|
||||||
.date {
|
.date {
|
||||||
font-size: 1.3em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="date-time-weather">
|
<div class="date-time-weather">
|
||||||
<div class="row date-time-container">
|
<div class="row date-time-container">
|
||||||
<DateTime :show-date="_showDate" :show-time="_showTime" :animate="animate"
|
<DateTime :show-date="_showDate" :show-time="_showTime" :show-seconds="_showSeconds" :animate="animate"
|
||||||
v-if="_showDate || _showTime" />
|
v-if="_showDate || _showTime" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -30,8 +30,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
import DateTime from "@/widgets/DateTime/Index";
|
import DateTime from "@/components/widgets/DateTime/Index";
|
||||||
import Weather from "@/widgets/Weather/Index";
|
import Weather from "@/components/widgets/Weather/Index";
|
||||||
import Sensor from "@/components/Sensor";
|
import Sensor from "@/components/Sensor";
|
||||||
|
|
||||||
// Widget to show date, time, weather and temperature information
|
// Widget to show date, time, weather and temperature information
|
||||||
|
@ -44,7 +44,7 @@ export default {
|
||||||
// Otherwise, it will be a static image.
|
// Otherwise, it will be a static image.
|
||||||
animate: {
|
animate: {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Size of the weather icon in pixels.
|
// Size of the weather icon in pixels.
|
||||||
|
@ -84,6 +84,12 @@ export default {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// If false then don't display the seconds.
|
||||||
|
showSeconds: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Name of the attribute on a received SensorDataChangeEvent that
|
// Name of the attribute on a received SensorDataChangeEvent that
|
||||||
// represents the temperature value to be rendered.
|
// represents the temperature value to be rendered.
|
||||||
sensorTemperatureAttr: {
|
sensorTemperatureAttr: {
|
||||||
|
@ -117,6 +123,10 @@ export default {
|
||||||
return this.parseBoolean(this.showTime)
|
return this.parseBoolean(this.showTime)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_showSeconds() {
|
||||||
|
return this.parseBoolean(this.showSeconds)
|
||||||
|
},
|
||||||
|
|
||||||
_showWeather() {
|
_showWeather() {
|
||||||
return this.parseBoolean(this.showWeather)
|
return this.parseBoolean(this.showWeather)
|
||||||
},
|
},
|
|
@ -0,0 +1,292 @@
|
||||||
|
<template>
|
||||||
|
<div class="image-carousel">
|
||||||
|
<Loading v-if="!images.length" />
|
||||||
|
<div ref="background" class="background" />
|
||||||
|
<img ref="img" :src="imgURL" alt="Your carousel images"
|
||||||
|
:style="{display: !images.length ? 'none' : 'block'}">
|
||||||
|
|
||||||
|
<div class="row info-container" v-if="_showDate || _showTime">
|
||||||
|
<div class="col-6 weather-container">
|
||||||
|
<span v-if="!_showWeather"> </span>
|
||||||
|
<Weather :show-icon="_showWeatherIcon" :show-summary="_showWeatherSummary" :show-temperature="_showTemperature"
|
||||||
|
:icon-color="weatherIconColor" :icon-size="weatherIconSize" :animate="_animateWeatherIcon" v-else />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6 date-time-container">
|
||||||
|
<DateTime :show-date="_showDate" :show-time="_showTime" :show-seconds="_showSeconds"
|
||||||
|
v-if="_showTime || _showDate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import DateTime from "@/components/widgets/DateTime/Index";
|
||||||
|
import Weather from "@/components/widgets/Weather/Index";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ImageCarousel",
|
||||||
|
components: {Weather, DateTime, Loading},
|
||||||
|
mixins: [Utils],
|
||||||
|
props: {
|
||||||
|
// Images directory
|
||||||
|
imgDir: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Refresh interval in seconds.
|
||||||
|
refreshSeconds: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 15,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show the current date on top of the images
|
||||||
|
showDate: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show the current time on top of the images
|
||||||
|
showTime: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display the seconds.
|
||||||
|
showSeconds: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display weather info.
|
||||||
|
showWeather: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then temperature won't be displayed.
|
||||||
|
showTemperature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display the weather state icon.
|
||||||
|
showWeatherIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display the weather summary text.
|
||||||
|
showWeatherSummary: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Weather con color.
|
||||||
|
weatherIconColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'white',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Size of the weather icon in pixels.
|
||||||
|
weatherIconSize: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 40,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then the weather icon will be animated.
|
||||||
|
// Otherwise, it will be a static image.
|
||||||
|
animateWeatherIcon: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
images: [],
|
||||||
|
currentImage: undefined,
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
imgURL() {
|
||||||
|
let port = 8008
|
||||||
|
if ('backend.http' in this.$root.config && 'port' in this.$root.config['backend.http']) {
|
||||||
|
port = this.$root.config['backend.http'].port
|
||||||
|
}
|
||||||
|
|
||||||
|
return '//' + window.location.hostname + ':' + port + this.currentImage
|
||||||
|
},
|
||||||
|
|
||||||
|
_showDate() {
|
||||||
|
return this.parseBoolean(this.showDate)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showTime() {
|
||||||
|
return this.parseBoolean(this.showTime)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showSeconds() {
|
||||||
|
return this.parseBoolean(this.showSeconds)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showTemperature() {
|
||||||
|
return this.parseBoolean(this.showTemperature)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showWeather() {
|
||||||
|
return this.parseBoolean(this.showWeather)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showWeatherIcon() {
|
||||||
|
return this.parseBoolean(this.showWeatherIcon)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showWeatherSummary() {
|
||||||
|
return this.parseBoolean(this.showWeatherSummary)
|
||||||
|
},
|
||||||
|
|
||||||
|
_animateWeatherIcon() {
|
||||||
|
return this.parseBoolean(this.animateWeatherIcon)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async refresh() {
|
||||||
|
if (!this.images.length) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.images = await this.request('utils.search_web_directory', {
|
||||||
|
directory: this.imgDir,
|
||||||
|
extensions: ['.jpg', '.jpeg', '.png'],
|
||||||
|
})
|
||||||
|
|
||||||
|
this.shuffleImages()
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.images.length) {
|
||||||
|
this.currentImage = this.images.pop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onNewImage() {
|
||||||
|
if (!this.$refs.img)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.$refs.background.style['background-image'] = 'url(' + this.imgURL + ')'
|
||||||
|
this.$refs.img.style.width = 'auto'
|
||||||
|
|
||||||
|
if (this.$refs.img.width > this.$refs.img.height) {
|
||||||
|
const ratio = this.$refs.img.width / this.$refs.img.height
|
||||||
|
if (4/3 <= ratio <= 16/9) {
|
||||||
|
this.$refs.img.style.width = '100%'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio <= 4/3) {
|
||||||
|
this.$refs.img.style.height = '100%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
shuffleImages() {
|
||||||
|
for (let i=this.images.length-1; i > 0; i--) {
|
||||||
|
let j = Math.floor(Math.random() * (i+1))
|
||||||
|
let x = this.images[i]
|
||||||
|
this.images[i] = this.images[j]
|
||||||
|
this.images[j] = x
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$refs.img.addEventListener('load', this.onNewImage)
|
||||||
|
this.$refs.img.addEventListener('error', this.refresh)
|
||||||
|
|
||||||
|
this.refresh()
|
||||||
|
setInterval(this.refresh, Math.round(this.refreshSeconds * 1000))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.image-carousel {
|
||||||
|
width: calc(100% + 1.5em);
|
||||||
|
height: calc(100% + 1.5em);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: black;
|
||||||
|
margin: -0.75em 0.75em 0.75em -0.75em !important;
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: black;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
filter: blur(13px);
|
||||||
|
-webkit-filter: blur(13px);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
max-height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
z-index: 10;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 3px 3px 4px black;
|
||||||
|
font-size: 1.25em;
|
||||||
|
margin: 0.5em;
|
||||||
|
padding: 0 1em;
|
||||||
|
|
||||||
|
.date-time {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.info-container {
|
||||||
|
.weather-container {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
justify-content: left;
|
||||||
|
margin-bottom: -0.5em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -31,8 +31,13 @@ export default {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.row {
|
.row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 49%;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 1%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -3,8 +3,9 @@
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
|
|
||||||
<h1 v-else>
|
<h1 v-else>
|
||||||
<skycons :condition="weatherIcon" :paused="animate" :size="iconSize" v-if="weatherIcon" />
|
<skycons :condition="weatherIcon" :paused="!animate" :size="iconSize" :color="iconColor"
|
||||||
<span class="temperature" v-if="weather">
|
v-if="_showIcon && weatherIcon" />
|
||||||
|
<span class="temperature" v-if="_showTemperature && weather">
|
||||||
{{ Math.round(parseFloat(weather.temperature)) + '°' }}
|
{{ Math.round(parseFloat(weather.temperature)) + '°' }}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -28,7 +29,7 @@ export default {
|
||||||
// Otherwise, it will be a static image.
|
// Otherwise, it will be a static image.
|
||||||
animate: {
|
animate: {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Size of the weather icon in pixels.
|
// Size of the weather icon in pixels.
|
||||||
|
@ -38,12 +39,30 @@ export default {
|
||||||
default: 50,
|
default: 50,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Icon color.
|
||||||
|
iconColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then the weather icon won't be displayed.
|
||||||
|
showIcon: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
// If false then the weather summary won't be displayed.
|
// If false then the weather summary won't be displayed.
|
||||||
showSummary: {
|
showSummary: {
|
||||||
required: false,
|
required: false,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// If false then the temperature won't be displayed.
|
||||||
|
showTemperature: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Refresh interval in seconds.
|
// Refresh interval in seconds.
|
||||||
refreshSeconds: {
|
refreshSeconds: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -64,6 +83,14 @@ export default {
|
||||||
_showSummary() {
|
_showSummary() {
|
||||||
return this.parseBoolean(this.showSummary)
|
return this.parseBoolean(this.showSummary)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_showIcon() {
|
||||||
|
return this.parseBoolean(this.showIcon)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showTemperature() {
|
||||||
|
return this.parseBoolean(this.showTemperature)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -102,8 +129,8 @@ export default {
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.temperature {
|
.temperature {
|
|
@ -25,13 +25,7 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
classes() {
|
classes() {
|
||||||
let classes = ['widget', 'column']
|
return (this.class && this.class.length ? this.class.split(' ') : ['col-3']).concat(['widget', 'column'])
|
||||||
if (this.class && this.class.length)
|
|
||||||
classes = classes.concat(this.class.split(' '))
|
|
||||||
else
|
|
||||||
classes = classes.concat('col-3')
|
|
||||||
|
|
||||||
return classes
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -39,9 +33,9 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.widget {
|
.widget {
|
||||||
|
height: calc(100% - 1em);
|
||||||
background: $background-color;
|
background: $background-color;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-bottom: 1em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
|
@ -49,16 +43,4 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
|
box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
@media screen and (max-width: $tablet){
|
|
||||||
.widget {
|
|
||||||
height: calc(100% - 1em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: $desktop){
|
|
||||||
.widget {
|
|
||||||
height: calc(50% - 1em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -16,8 +16,8 @@
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Utils from '@/Utils'
|
import Utils from '@/Utils'
|
||||||
import Loading from "@/components/Loading";
|
import Loading from "@/components/Loading";
|
||||||
import Row from "@/widgets/Row";
|
import Row from "@/components/widgets/Row";
|
||||||
import Widget from "@/widgets/Widget";
|
import Widget from "@/components/widgets/Widget";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
|
@ -60,7 +60,7 @@ export default {
|
||||||
class: row.attributes.class ? row.attributes.class.nodeValue : undefined,
|
class: row.attributes.class ? row.attributes.class.nodeValue : undefined,
|
||||||
widgets: [...row.children].map((el) => {
|
widgets: [...row.children].map((el) => {
|
||||||
const component = defineAsyncComponent(
|
const component = defineAsyncComponent(
|
||||||
() => import(`@/widgets/${el.nodeName}/Index`)
|
() => import(`@/components/widgets/${el.nodeName}/Index`)
|
||||||
)
|
)
|
||||||
|
|
||||||
const style = el.attributes.style ? el.attributes.style.nodeValue : undefined
|
const style = el.attributes.style ? el.attributes.style.nodeValue : undefined
|
||||||
|
@ -120,6 +120,7 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1em 1em 0 1em;
|
padding: 1em 1em 0 1em;
|
||||||
background: $dashboard-bg;
|
background: $dashboard-bg;
|
||||||
|
|
Loading…
Reference in a new issue