Add link generation buttons
This commit is contained in:
437
immich_album_watcher/button.py
Normal file
437
immich_album_watcher/button.py
Normal file
@@ -0,0 +1,437 @@
|
||||
"""Button platform for Immich Album Watcher."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (
|
||||
ATTR_ALBUM_ID,
|
||||
ATTR_ALBUM_PROTECTED_URL,
|
||||
CONF_ALBUM_ID,
|
||||
CONF_ALBUM_NAME,
|
||||
CONF_HUB_NAME,
|
||||
DEFAULT_SHARE_PASSWORD,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Immich Album Watcher button entities from a config entry."""
|
||||
# Iterate through all album subentries
|
||||
for subentry_id, subentry in entry.subentries.items():
|
||||
subentry_data = hass.data[DOMAIN][entry.entry_id]["subentries"].get(subentry_id)
|
||||
if not subentry_data:
|
||||
_LOGGER.error("Subentry data not found for %s", subentry_id)
|
||||
continue
|
||||
|
||||
coordinator = subentry_data.coordinator
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
ImmichCreateShareLinkButton(coordinator, entry, subentry),
|
||||
ImmichDeleteShareLinkButton(coordinator, entry, subentry),
|
||||
ImmichCreateProtectedLinkButton(coordinator, entry, subentry),
|
||||
ImmichDeleteProtectedLinkButton(coordinator, entry, subentry),
|
||||
],
|
||||
config_subentry_id=subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class ImmichCreateShareLinkButton(
|
||||
CoordinatorEntity[ImmichAlbumWatcherCoordinator], ButtonEntity
|
||||
):
|
||||
"""Button entity for creating an unprotected share link."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:link-plus"
|
||||
_attr_translation_key = "create_share_link"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ImmichAlbumWatcherCoordinator,
|
||||
entry: ConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
) -> None:
|
||||
"""Initialize the button entity."""
|
||||
super().__init__(coordinator)
|
||||
self._entry = entry
|
||||
self._subentry = subentry
|
||||
self._album_id = subentry.data[CONF_ALBUM_ID]
|
||||
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
|
||||
self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
|
||||
unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
|
||||
self._attr_unique_id = f"{unique_id_prefix}_create_share_link"
|
||||
|
||||
@property
|
||||
def _album_data(self) -> AlbumData | None:
|
||||
"""Get the album data from coordinator."""
|
||||
return self.coordinator.data
|
||||
|
||||
@property
|
||||
def translation_placeholders(self) -> dict[str, str]:
|
||||
"""Return translation placeholders."""
|
||||
if self._album_data:
|
||||
return {"album_name": self._album_data.name}
|
||||
return {"album_name": self._album_name}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available.
|
||||
|
||||
Only available when there is no unprotected link.
|
||||
"""
|
||||
if not self.coordinator.last_update_success or self._album_data is None:
|
||||
return False
|
||||
# Only available if there's no unprotected link yet
|
||||
return not self.coordinator.has_unprotected_link()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info - one device per album."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._subentry.subentry_id)},
|
||||
name=self._album_name,
|
||||
manufacturer="Immich",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
via_device=(DOMAIN, self._entry.entry_id),
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return extra state attributes."""
|
||||
if not self._album_data:
|
||||
return {}
|
||||
|
||||
return {
|
||||
ATTR_ALBUM_ID: self._album_data.id,
|
||||
}
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press - create share link."""
|
||||
if self.coordinator.has_unprotected_link():
|
||||
_LOGGER.warning(
|
||||
"Album %s already has an unprotected share link",
|
||||
self._album_name,
|
||||
)
|
||||
return
|
||||
|
||||
success = await self.coordinator.async_create_shared_link()
|
||||
|
||||
if success:
|
||||
await self.coordinator.async_refresh()
|
||||
else:
|
||||
_LOGGER.error("Failed to create share link for album %s", self._album_id)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ImmichDeleteShareLinkButton(
|
||||
CoordinatorEntity[ImmichAlbumWatcherCoordinator], ButtonEntity
|
||||
):
|
||||
"""Button entity for deleting an unprotected share link."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:link-off"
|
||||
_attr_translation_key = "delete_share_link"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ImmichAlbumWatcherCoordinator,
|
||||
entry: ConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
) -> None:
|
||||
"""Initialize the button entity."""
|
||||
super().__init__(coordinator)
|
||||
self._entry = entry
|
||||
self._subentry = subentry
|
||||
self._album_id = subentry.data[CONF_ALBUM_ID]
|
||||
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
|
||||
self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
|
||||
unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
|
||||
self._attr_unique_id = f"{unique_id_prefix}_delete_share_link"
|
||||
|
||||
@property
|
||||
def _album_data(self) -> AlbumData | None:
|
||||
"""Get the album data from coordinator."""
|
||||
return self.coordinator.data
|
||||
|
||||
@property
|
||||
def translation_placeholders(self) -> dict[str, str]:
|
||||
"""Return translation placeholders."""
|
||||
if self._album_data:
|
||||
return {"album_name": self._album_data.name}
|
||||
return {"album_name": self._album_name}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available.
|
||||
|
||||
Only available when there is an unprotected link.
|
||||
"""
|
||||
if not self.coordinator.last_update_success or self._album_data is None:
|
||||
return False
|
||||
# Only available if there's an unprotected link to delete
|
||||
return self.coordinator.has_unprotected_link()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info - one device per album."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._subentry.subentry_id)},
|
||||
name=self._album_name,
|
||||
manufacturer="Immich",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
via_device=(DOMAIN, self._entry.entry_id),
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return extra state attributes."""
|
||||
if not self._album_data:
|
||||
return {}
|
||||
|
||||
attrs = {
|
||||
ATTR_ALBUM_ID: self._album_data.id,
|
||||
}
|
||||
|
||||
public_url = self.coordinator.get_public_url()
|
||||
if public_url:
|
||||
attrs[ATTR_ALBUM_PROTECTED_URL] = public_url
|
||||
|
||||
return attrs
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press - delete share link."""
|
||||
link_id = self.coordinator.get_unprotected_link_id()
|
||||
if not link_id:
|
||||
_LOGGER.warning(
|
||||
"No unprotected share link found for album %s",
|
||||
self._album_name,
|
||||
)
|
||||
return
|
||||
|
||||
success = await self.coordinator.async_delete_shared_link(link_id)
|
||||
|
||||
if success:
|
||||
await self.coordinator.async_refresh()
|
||||
else:
|
||||
_LOGGER.error("Failed to delete share link for album %s", self._album_id)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ImmichCreateProtectedLinkButton(
|
||||
CoordinatorEntity[ImmichAlbumWatcherCoordinator], ButtonEntity
|
||||
):
|
||||
"""Button entity for creating a protected (password) share link."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:link-lock"
|
||||
_attr_translation_key = "create_protected_link"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ImmichAlbumWatcherCoordinator,
|
||||
entry: ConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
) -> None:
|
||||
"""Initialize the button entity."""
|
||||
super().__init__(coordinator)
|
||||
self._entry = entry
|
||||
self._subentry = subentry
|
||||
self._album_id = subentry.data[CONF_ALBUM_ID]
|
||||
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
|
||||
self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
|
||||
unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
|
||||
self._attr_unique_id = f"{unique_id_prefix}_create_protected_link"
|
||||
|
||||
@property
|
||||
def _album_data(self) -> AlbumData | None:
|
||||
"""Get the album data from coordinator."""
|
||||
return self.coordinator.data
|
||||
|
||||
@property
|
||||
def translation_placeholders(self) -> dict[str, str]:
|
||||
"""Return translation placeholders."""
|
||||
if self._album_data:
|
||||
return {"album_name": self._album_data.name}
|
||||
return {"album_name": self._album_name}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available.
|
||||
|
||||
Only available when there is no protected link.
|
||||
"""
|
||||
if not self.coordinator.last_update_success or self._album_data is None:
|
||||
return False
|
||||
# Only available if there's no protected link yet
|
||||
return not self.coordinator.has_protected_link()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info - one device per album."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._subentry.subentry_id)},
|
||||
name=self._album_name,
|
||||
manufacturer="Immich",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
via_device=(DOMAIN, self._entry.entry_id),
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return extra state attributes."""
|
||||
if not self._album_data:
|
||||
return {}
|
||||
|
||||
return {
|
||||
ATTR_ALBUM_ID: self._album_data.id,
|
||||
}
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press - create protected share link."""
|
||||
if self.coordinator.has_protected_link():
|
||||
_LOGGER.warning(
|
||||
"Album %s already has a protected share link",
|
||||
self._album_name,
|
||||
)
|
||||
return
|
||||
|
||||
success = await self.coordinator.async_create_shared_link(
|
||||
password=DEFAULT_SHARE_PASSWORD
|
||||
)
|
||||
|
||||
if success:
|
||||
await self.coordinator.async_refresh()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Failed to create protected share link for album %s", self._album_id
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ImmichDeleteProtectedLinkButton(
|
||||
CoordinatorEntity[ImmichAlbumWatcherCoordinator], ButtonEntity
|
||||
):
|
||||
"""Button entity for deleting a protected (password) share link."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:link-off"
|
||||
_attr_translation_key = "delete_protected_link"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ImmichAlbumWatcherCoordinator,
|
||||
entry: ConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
) -> None:
|
||||
"""Initialize the button entity."""
|
||||
super().__init__(coordinator)
|
||||
self._entry = entry
|
||||
self._subentry = subentry
|
||||
self._album_id = subentry.data[CONF_ALBUM_ID]
|
||||
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
|
||||
self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
|
||||
unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
|
||||
self._attr_unique_id = f"{unique_id_prefix}_delete_protected_link"
|
||||
|
||||
@property
|
||||
def _album_data(self) -> AlbumData | None:
|
||||
"""Get the album data from coordinator."""
|
||||
return self.coordinator.data
|
||||
|
||||
@property
|
||||
def translation_placeholders(self) -> dict[str, str]:
|
||||
"""Return translation placeholders."""
|
||||
if self._album_data:
|
||||
return {"album_name": self._album_data.name}
|
||||
return {"album_name": self._album_name}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available.
|
||||
|
||||
Only available when there is a protected link.
|
||||
"""
|
||||
if not self.coordinator.last_update_success or self._album_data is None:
|
||||
return False
|
||||
# Only available if there's a protected link to delete
|
||||
return self.coordinator.has_protected_link()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info - one device per album."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._subentry.subentry_id)},
|
||||
name=self._album_name,
|
||||
manufacturer="Immich",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
via_device=(DOMAIN, self._entry.entry_id),
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return extra state attributes."""
|
||||
if not self._album_data:
|
||||
return {}
|
||||
|
||||
attrs = {
|
||||
ATTR_ALBUM_ID: self._album_data.id,
|
||||
}
|
||||
|
||||
protected_url = self.coordinator.get_protected_url()
|
||||
if protected_url:
|
||||
attrs[ATTR_ALBUM_PROTECTED_URL] = protected_url
|
||||
|
||||
return attrs
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press - delete protected share link."""
|
||||
link_id = self.coordinator.get_protected_link_id()
|
||||
if not link_id:
|
||||
_LOGGER.warning(
|
||||
"No protected share link found for album %s",
|
||||
self._album_name,
|
||||
)
|
||||
return
|
||||
|
||||
success = await self.coordinator.async_delete_shared_link(link_id)
|
||||
|
||||
if success:
|
||||
await self.coordinator.async_refresh()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Failed to delete protected share link for album %s", self._album_id
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.async_write_ha_state()
|
||||
@@ -20,6 +20,7 @@ SUBENTRY_TYPE_ALBUM: Final = "album"
|
||||
# Defaults
|
||||
DEFAULT_SCAN_INTERVAL: Final = 60 # seconds
|
||||
NEW_ASSETS_RESET_DELAY: Final = 300 # 5 minutes
|
||||
DEFAULT_SHARE_PASSWORD: Final = "immich123"
|
||||
|
||||
# Events
|
||||
EVENT_ALBUM_CHANGED: Final = f"{DOMAIN}_album_changed"
|
||||
@@ -61,7 +62,7 @@ ASSET_TYPE_IMAGE: Final = "IMAGE"
|
||||
ASSET_TYPE_VIDEO: Final = "VIDEO"
|
||||
|
||||
# Platforms
|
||||
PLATFORMS: Final = ["sensor", "binary_sensor", "camera", "text"]
|
||||
PLATFORMS: Final = ["sensor", "binary_sensor", "camera", "text", "button"]
|
||||
|
||||
# Services
|
||||
SERVICE_REFRESH: Final = "refresh"
|
||||
|
||||
@@ -621,3 +621,92 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
if self.data:
|
||||
self.data.has_new_assets = False
|
||||
self.data.last_change_time = None
|
||||
|
||||
def has_unprotected_link(self) -> bool:
|
||||
"""Check if album has an unprotected (accessible) shared link."""
|
||||
return len(self._get_accessible_links()) > 0
|
||||
|
||||
def has_protected_link(self) -> bool:
|
||||
"""Check if album has a protected (password) shared link."""
|
||||
return len(self._get_protected_links()) > 0
|
||||
|
||||
def get_unprotected_link_id(self) -> str | None:
|
||||
"""Get the ID of the first unprotected link."""
|
||||
accessible_links = self._get_accessible_links()
|
||||
if accessible_links:
|
||||
return accessible_links[0].id
|
||||
return None
|
||||
|
||||
async def async_create_shared_link(self, password: str | None = None) -> bool:
|
||||
"""Create a new shared link for the album via Immich API."""
|
||||
if self._session is None:
|
||||
self._session = async_get_clientsession(self.hass)
|
||||
|
||||
headers = {
|
||||
"x-api-key": self._api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"albumId": self._album_id,
|
||||
"type": "ALBUM",
|
||||
"allowDownload": True,
|
||||
"allowUpload": False,
|
||||
"showMetadata": True,
|
||||
}
|
||||
|
||||
if password:
|
||||
payload["password"] = password
|
||||
|
||||
try:
|
||||
async with self._session.post(
|
||||
f"{self._url}/api/shared-links",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
) as response:
|
||||
if response.status == 201:
|
||||
_LOGGER.info(
|
||||
"Successfully created shared link for album %s",
|
||||
self._album_name,
|
||||
)
|
||||
await self._async_fetch_shared_links()
|
||||
return True
|
||||
else:
|
||||
error_text = await response.text()
|
||||
_LOGGER.error(
|
||||
"Failed to create shared link: HTTP %s - %s",
|
||||
response.status,
|
||||
error_text,
|
||||
)
|
||||
return False
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Error creating shared link: %s", err)
|
||||
return False
|
||||
|
||||
async def async_delete_shared_link(self, link_id: str) -> bool:
|
||||
"""Delete a shared link via Immich API."""
|
||||
if self._session is None:
|
||||
self._session = async_get_clientsession(self.hass)
|
||||
|
||||
headers = {"x-api-key": self._api_key}
|
||||
|
||||
try:
|
||||
async with self._session.delete(
|
||||
f"{self._url}/api/shared-links/{link_id}",
|
||||
headers=headers,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
_LOGGER.info("Successfully deleted shared link")
|
||||
await self._async_fetch_shared_links()
|
||||
return True
|
||||
else:
|
||||
error_text = await response.text()
|
||||
_LOGGER.error(
|
||||
"Failed to delete shared link: HTTP %s - %s",
|
||||
response.status,
|
||||
error_text,
|
||||
)
|
||||
return False
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Error deleting shared link: %s", err)
|
||||
return False
|
||||
|
||||
@@ -68,7 +68,6 @@ async def async_setup_entry(
|
||||
ImmichAlbumVideoCountSensor(coordinator, entry, subentry),
|
||||
ImmichAlbumLastUpdatedSensor(coordinator, entry, subentry),
|
||||
ImmichAlbumCreatedSensor(coordinator, entry, subentry),
|
||||
ImmichAlbumPeopleSensor(coordinator, entry, subentry),
|
||||
ImmichAlbumPublicUrlSensor(coordinator, entry, subentry),
|
||||
ImmichAlbumProtectedUrlSensor(coordinator, entry, subentry),
|
||||
ImmichAlbumProtectedPasswordSensor(coordinator, entry, subentry),
|
||||
@@ -322,41 +321,6 @@ class ImmichAlbumCreatedSensor(ImmichAlbumBaseSensor):
|
||||
return None
|
||||
|
||||
|
||||
class ImmichAlbumPeopleSensor(ImmichAlbumBaseSensor):
|
||||
"""Sensor representing people detected in an Immich album."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_icon = "mdi:account-group"
|
||||
_attr_translation_key = "album_people_count"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ImmichAlbumWatcherCoordinator,
|
||||
entry: ConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, entry, subentry)
|
||||
self._attr_unique_id = f"{self._unique_id_prefix}_people_count"
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the state of the sensor (number of unique people)."""
|
||||
if self._album_data:
|
||||
return len(self._album_data.people)
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return extra state attributes."""
|
||||
if not self._album_data:
|
||||
return {}
|
||||
|
||||
return {
|
||||
ATTR_PEOPLE: list(self._album_data.people),
|
||||
}
|
||||
|
||||
|
||||
class ImmichAlbumPublicUrlSensor(ImmichAlbumBaseSensor):
|
||||
"""Sensor representing an Immich album public URL."""
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
"album_created": {
|
||||
"name": "{album_name}: Created"
|
||||
},
|
||||
"album_people_count": {
|
||||
"name": "{album_name}: People Count"
|
||||
},
|
||||
"album_public_url": {
|
||||
"name": "{album_name}: Public URL"
|
||||
},
|
||||
@@ -43,6 +40,20 @@
|
||||
"album_protected_password_edit": {
|
||||
"name": "{album_name}: Share Password"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"create_share_link": {
|
||||
"name": "{album_name}: Create Share Link"
|
||||
},
|
||||
"delete_share_link": {
|
||||
"name": "{album_name}: Delete Share Link"
|
||||
},
|
||||
"create_protected_link": {
|
||||
"name": "{album_name}: Create Protected Link"
|
||||
},
|
||||
"delete_protected_link": {
|
||||
"name": "{album_name}: Delete Protected Link"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
"album_created": {
|
||||
"name": "{album_name}: Дата создания"
|
||||
},
|
||||
"album_people_count": {
|
||||
"name": "{album_name}: Число людей"
|
||||
},
|
||||
"album_public_url": {
|
||||
"name": "{album_name}: Публичная ссылка"
|
||||
},
|
||||
@@ -43,6 +40,20 @@
|
||||
"album_protected_password_edit": {
|
||||
"name": "{album_name}: Пароль ссылки"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"create_share_link": {
|
||||
"name": "{album_name}: Создать ссылку"
|
||||
},
|
||||
"delete_share_link": {
|
||||
"name": "{album_name}: Удалить ссылку"
|
||||
},
|
||||
"create_protected_link": {
|
||||
"name": "{album_name}: Создать защищённую ссылку"
|
||||
},
|
||||
"delete_protected_link": {
|
||||
"name": "{album_name}: Удалить защищённую ссылку"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
||||
Reference in New Issue
Block a user