1.5.1原版
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package run.halo.photos;
|
||||
|
||||
/**
|
||||
* Static variable keys for view model.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public enum ModelConst {
|
||||
;
|
||||
public static final String TEMPLATE_ID = "_templateId";
|
||||
public static final Integer DEFAULT_PAGE_SIZE = 10;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
/**
|
||||
* @author ryanwang
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "core.halo.run", version = "v1alpha1", kind = "Photo", plural = "photos",
|
||||
singular = "photo")
|
||||
public class Photo extends AbstractExtension {
|
||||
|
||||
private PhotoSpec spec;
|
||||
|
||||
@Data
|
||||
public static class PhotoSpec {
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private String displayName;
|
||||
|
||||
private String description;
|
||||
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private String url;
|
||||
|
||||
private String cover;
|
||||
|
||||
private Integer priority;
|
||||
|
||||
@Schema(requiredMode = REQUIRED, pattern = "^\\S+$")
|
||||
private String groupName;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isDeleted() {
|
||||
return Objects.equals(true,
|
||||
getMetadata().getDeletionTimestamp() != null
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.photos.service.PhotoService;
|
||||
|
||||
/**
|
||||
* A custom endpoint for {@link Photo}.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PhotoEndpoint implements CustomEndpoint {
|
||||
|
||||
private final PhotoService photoService;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "console.api.photo.halo.run/v1alpha1/Photo";
|
||||
return route()
|
||||
.GET("photos", this::listPhoto,
|
||||
builder -> {
|
||||
builder.operationId("ListPhotos")
|
||||
.description("List photos.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder().implementation(
|
||||
ListResult.generateGenericClass(Photo.class)));
|
||||
|
||||
PhotoQuery.buildParameters(builder);
|
||||
}
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("console.api.photo.halo.run/v1alpha1");
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> listPhoto(ServerRequest serverRequest) {
|
||||
PhotoQuery query = new PhotoQuery(serverRequest.exchange());
|
||||
return photoService.listPhoto(query)
|
||||
.flatMap(photos -> ServerResponse.ok().bodyValue(photos));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
/**
|
||||
* @author ryanwang
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "core.halo.run", version = "v1alpha1", kind = "PhotoGroup",
|
||||
plural = "photogroups", singular = "photogroup")
|
||||
public class PhotoGroup extends AbstractExtension {
|
||||
|
||||
@Schema(required = true)
|
||||
private PhotoGroupSpec spec;
|
||||
|
||||
@Schema
|
||||
private PostGroupStatus status;
|
||||
|
||||
@Data
|
||||
public static class PhotoGroupSpec {
|
||||
@Schema(required = true)
|
||||
private String displayName;
|
||||
|
||||
private Integer priority;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public PostGroupStatus getStatusOrDefault() {
|
||||
if (this.status == null) {
|
||||
this.status = new PostGroupStatus();
|
||||
}
|
||||
return this.status;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PostGroupStatus {
|
||||
|
||||
public Integer photoCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||
import run.halo.photos.service.PhotoGroupService;
|
||||
|
||||
/**
|
||||
* A custom endpoint for {@link Photo}.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PhotoGroupEndpoint implements CustomEndpoint {
|
||||
|
||||
private final PhotoGroupService photoGroupService;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
return route()
|
||||
.GET("photogroups", this::listPhotoGroup,
|
||||
builder -> {
|
||||
builder.operationId("ListPhotos")
|
||||
.description("List photos.")
|
||||
.response(responseBuilder().implementation(
|
||||
ListResult.generateGenericClass(PhotoGroup.class))
|
||||
);
|
||||
PhotoQuery.buildParameters(builder);
|
||||
}
|
||||
)
|
||||
.DELETE("photogroups/{name}", this::deletePhotoGroup,
|
||||
builder -> builder.operationId("DeletePhotoGroup")
|
||||
.description("Delete photo group.")
|
||||
.parameter(parameterBuilder()
|
||||
.name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.description("Photo group name")
|
||||
.implementation(String.class)
|
||||
.required(true)
|
||||
)
|
||||
.response(responseBuilder().implementation(PhotoGroup.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("console.api.photo.halo.run/v1alpha1");
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> deletePhotoGroup(ServerRequest serverRequest) {
|
||||
String name = serverRequest.pathVariable("name");
|
||||
if (StringUtils.isBlank(name)) {
|
||||
throw new ServerWebInputException("Photo group name must not be blank.");
|
||||
}
|
||||
return photoGroupService.deletePhotoGroup(name)
|
||||
.flatMap(photoGroup -> ServerResponse.ok().bodyValue(photoGroup));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> listPhotoGroup(ServerRequest serverRequest) {
|
||||
var request = new PhotoQuery(serverRequest.exchange());
|
||||
return photoGroupService.listPhotoGroup(request)
|
||||
.flatMap(photoGroups -> ServerResponse.ok().bodyValue(photoGroups));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.index.IndexSpec;
|
||||
import run.halo.app.plugin.BasePlugin;
|
||||
import run.halo.app.plugin.PluginContext;
|
||||
|
||||
/**
|
||||
* @author ryanwang
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PhotoPlugin extends BasePlugin {
|
||||
private final SchemeManager schemeManager;
|
||||
|
||||
public PhotoPlugin(PluginContext pluginContext, SchemeManager schemeManager) {
|
||||
super(pluginContext);
|
||||
this.schemeManager = schemeManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
schemeManager.register(Photo.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.groupName")
|
||||
.setIndexFunc(simpleAttribute(Photo.class, photo ->
|
||||
photo.getSpec() == null ? "" : photo.getSpec().getGroupName()
|
||||
))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.displayName")
|
||||
.setIndexFunc(simpleAttribute(Photo.class, photo ->
|
||||
photo.getSpec() == null ? "" : photo.getSpec().getDisplayName()
|
||||
))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.priority")
|
||||
.setIndexFunc(simpleAttribute(Photo.class, photo ->
|
||||
photo.getSpec() == null || photo.getSpec().getPriority() == null
|
||||
? String.valueOf(0) : photo.getSpec().getPriority().toString()
|
||||
))
|
||||
);
|
||||
});
|
||||
schemeManager.register(PhotoGroup.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.priority")
|
||||
.setIndexFunc(simpleAttribute(PhotoGroup.class, group ->
|
||||
group.getSpec() == null || group.getSpec().getPriority() == null
|
||||
? String.valueOf(0) : group.getSpec().getPriority().toString()
|
||||
))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
schemeManager.unregister(schemeManager.get(Photo.class));
|
||||
schemeManager.unregister(schemeManager.get(PhotoGroup.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.springdoc.core.fn.builders.operation.Builder;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.SortableRequest;
|
||||
|
||||
/**
|
||||
* A query object for {@link Photo} list.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class PhotoQuery extends SortableRequest {
|
||||
|
||||
public PhotoQuery(ServerWebExchange exchange) {
|
||||
super(exchange);
|
||||
}
|
||||
|
||||
@Schema(description = "Photos filtered by group.")
|
||||
public String getGroup() {
|
||||
return queryParams.getFirst("group");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Schema(description = "Photos filtered by keyword.")
|
||||
public String getKeyword() {
|
||||
return queryParams.getFirst("keyword");
|
||||
}
|
||||
|
||||
public static void buildParameters(Builder builder) {
|
||||
IListRequest.buildParameters(builder);
|
||||
builder.parameter(sortParameter())
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.QUERY)
|
||||
.name("keyword")
|
||||
.description("Photos filtered by keyword.")
|
||||
.implementation(String.class)
|
||||
.required(false))
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.QUERY)
|
||||
.name("group")
|
||||
.description("photo group name")
|
||||
.implementation(String.class)
|
||||
.required(false))
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author LIlGG
|
||||
*/
|
||||
@Data
|
||||
public class PhotoRequest {
|
||||
private String group;
|
||||
|
||||
private Photo photo;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.plugin.ReactiveSettingFetcher;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
import run.halo.photos.finders.PhotoFinder;
|
||||
import run.halo.photos.vo.PhotoGroupVo;
|
||||
import run.halo.photos.vo.PhotoVo;
|
||||
|
||||
/**
|
||||
* Provides a <code>/photos</code> route for the topic end to handle routing.
|
||||
*
|
||||
* @author LIlGG
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PhotoRouter {
|
||||
|
||||
private static final String GROUP_PARAM = "group";
|
||||
|
||||
private PhotoFinder photoFinder;
|
||||
|
||||
private final ReactiveSettingFetcher settingFetcher;
|
||||
|
||||
/**
|
||||
* Provides a <code>/photos</code> route for the topic end to handle routing.
|
||||
*
|
||||
* @return a {@link RouterFunction} instance
|
||||
*/
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> photoRouter() {
|
||||
return route(GET("/photos").or(GET("/photos/page/{page:\\d+}")),
|
||||
handlerFunction()
|
||||
);
|
||||
}
|
||||
|
||||
private HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> ServerResponse.ok().render("photos",
|
||||
Map.of("groups", photoGroups(),
|
||||
"photos", photoList(request),
|
||||
ModelConst.TEMPLATE_ID, "photos",
|
||||
"title", getPhotosTitle()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<PhotoVo>> photoList(ServerRequest request) {
|
||||
String path = request.path();
|
||||
int pageNum = pageNumInPathVariable(request);
|
||||
String group = groupPathQueryParam(request);
|
||||
return this.settingFetcher.get("base")
|
||||
.map(item -> item.get("pageSize").asInt(10))
|
||||
.defaultIfEmpty(10)
|
||||
.flatMap(pageSize -> photoFinder.list(pageNum, pageSize, group)
|
||||
.map(list -> new UrlContextListResult.Builder<PhotoVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(appendGroupParam(
|
||||
PageUrlUtils.nextPageUrl(path, totalPage(list)), group)
|
||||
)
|
||||
.prevUrl(appendGroupParam(PageUrlUtils.prevPageUrl(path), group))
|
||||
.build()
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private static String appendGroupParam(String path, String group) {
|
||||
return UriComponentsBuilder.fromPath(path)
|
||||
.queryParamIfPresent(GROUP_PARAM, Optional.ofNullable(group))
|
||||
.build()
|
||||
.toString();
|
||||
}
|
||||
|
||||
private int pageNumInPathVariable(ServerRequest request) {
|
||||
String page = request.pathVariables().get("page");
|
||||
return NumberUtils.toInt(page, 1);
|
||||
}
|
||||
|
||||
private String groupPathQueryParam(ServerRequest request) {
|
||||
return request.queryParam(GROUP_PARAM)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
Mono<String> getPhotosTitle() {
|
||||
return this.settingFetcher.get("base").map(
|
||||
setting -> setting.get("title").asText("图库")).defaultIfEmpty(
|
||||
"图库");
|
||||
}
|
||||
|
||||
private Mono<List<PhotoGroupVo>> photoGroups() {
|
||||
return photoFinder.groupBy().collectList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package run.halo.photos;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import org.springframework.util.comparator.Comparators;
|
||||
|
||||
/**
|
||||
* A sorter for {@link Photo}.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public enum PhotoSorter {
|
||||
DISPLAY_NAME,
|
||||
|
||||
CREATE_TIME;
|
||||
|
||||
static final Function<Photo, String> name = photo -> photo.getMetadata()
|
||||
.getName();
|
||||
|
||||
/**
|
||||
* Converts {@link Comparator} from {@link PhotoSorter} and ascending.
|
||||
*
|
||||
* @param sorter a {@link PhotoSorter}
|
||||
* @param ascending ascending if true, otherwise descending
|
||||
* @return a {@link Comparator} of {@link Photo}
|
||||
*/
|
||||
public static Comparator<Photo> from(PhotoSorter sorter,
|
||||
Boolean ascending) {
|
||||
if (Objects.equals(true, ascending)) {
|
||||
return from(sorter);
|
||||
}
|
||||
return from(sorter).reversed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@link Comparator} from {@link PhotoSorter}.
|
||||
*
|
||||
* @param sorter a {@link PhotoSorter}
|
||||
* @return a {@link Comparator} of {@link Photo}
|
||||
*/
|
||||
static Comparator<Photo> from(PhotoSorter sorter) {
|
||||
if (sorter == null) {
|
||||
return createTimeComparator();
|
||||
}
|
||||
if (CREATE_TIME.equals(sorter)) {
|
||||
Function<Photo, Instant> comparatorFunc
|
||||
= photo -> photo.getMetadata().getCreationTimestamp();
|
||||
return Comparator.comparing(comparatorFunc).thenComparing(name);
|
||||
}
|
||||
|
||||
if (DISPLAY_NAME.equals(sorter)) {
|
||||
Function<Photo, String> comparatorFunc = moment -> moment.getSpec()
|
||||
.getDisplayName();
|
||||
return Comparator.comparing(comparatorFunc, Comparators.nullsLow())
|
||||
.thenComparing(name);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unsupported sort value: " + sorter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@link PhotoSorter} from string.
|
||||
*
|
||||
* @param sort sort string
|
||||
* @return a {@link PhotoSorter}
|
||||
*/
|
||||
static PhotoSorter convertFrom(String sort) {
|
||||
for (PhotoSorter sorter : values()) {
|
||||
if (sorter.name().equalsIgnoreCase(sort)) {
|
||||
return sorter;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Comparator} of {@link Photo} by creation time.
|
||||
*
|
||||
* @return a {@link Comparator} of {@link Photo}
|
||||
*/
|
||||
static Comparator<Photo> createTimeComparator() {
|
||||
Function<Photo, Instant> comparatorFunc = photo -> photo.getMetadata()
|
||||
.getCreationTimestamp();
|
||||
return Comparator.comparing(comparatorFunc).thenComparing(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package run.halo.photos.finders;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.photos.vo.PhotoGroupVo;
|
||||
import run.halo.photos.vo.PhotoVo;
|
||||
|
||||
|
||||
/**
|
||||
* A finder for {@link run.halo.photos.Photo}.
|
||||
*
|
||||
* @author LIlGG
|
||||
*/
|
||||
public interface PhotoFinder {
|
||||
|
||||
/**
|
||||
* List all photos.
|
||||
*
|
||||
* @return a flux of photo vo
|
||||
*/
|
||||
Flux<PhotoVo> listAll();
|
||||
|
||||
/**
|
||||
* List photos by page.
|
||||
*
|
||||
* @param page page number
|
||||
* @param size page size
|
||||
* @return a mono of list result
|
||||
*/
|
||||
Mono<ListResult<PhotoVo>> list(Integer page, Integer size);
|
||||
|
||||
/**
|
||||
* List photos by page and group.
|
||||
*
|
||||
* @param page page number
|
||||
* @param size page size
|
||||
* @param group group name
|
||||
* @return a mono of list result
|
||||
*/
|
||||
Mono<ListResult<PhotoVo>> list(Integer page, Integer size, String group);
|
||||
|
||||
/**
|
||||
* List photos by group.
|
||||
*
|
||||
* @param group group name
|
||||
* @return a flux of photo vo
|
||||
*/
|
||||
Flux<PhotoVo> listBy(String group);
|
||||
|
||||
/**
|
||||
* List all groups.
|
||||
*
|
||||
* @return a flux of photo group vo
|
||||
*/
|
||||
Flux<PhotoGroupVo> groupBy();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package run.halo.photos.finders.impl;
|
||||
|
||||
import static org.springframework.data.domain.Sort.Order.asc;
|
||||
import static org.springframework.data.domain.Sort.Order.desc;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.photos.Photo;
|
||||
import run.halo.photos.PhotoGroup;
|
||||
import run.halo.photos.finders.PhotoFinder;
|
||||
import run.halo.photos.vo.PhotoGroupVo;
|
||||
import run.halo.photos.vo.PhotoVo;
|
||||
|
||||
/**
|
||||
* @author LIlGG
|
||||
*/
|
||||
@Finder("photoFinder")
|
||||
public class PhotoFInderImpl implements PhotoFinder {
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public PhotoFInderImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<PhotoVo> listAll() {
|
||||
return this.client.listAll(Photo.class, ListOptions.builder().build(), defaultSort())
|
||||
.map(PhotoVo::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<PhotoVo>> list(Integer page, Integer size) {
|
||||
return list(page, size, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<PhotoVo>> list(Integer page, Integer size, String group) {
|
||||
return pagePhoto(page, size, group);
|
||||
}
|
||||
|
||||
private Mono<ListResult<PhotoVo>> pagePhoto(Integer page, Integer size, String group) {
|
||||
var builder = ListOptions.builder();
|
||||
if (StringUtils.isNotEmpty(group)) {
|
||||
builder.andQuery(QueryFactory.equal("spec.groupName", group));
|
||||
}
|
||||
return client.listBy(Photo.class, builder.build(),
|
||||
PageRequestImpl.of(page, size, defaultSort()))
|
||||
.flatMap(listResult -> Flux.fromStream(listResult.get())
|
||||
.map(PhotoVo::from)
|
||||
.collectList()
|
||||
.map(list -> new ListResult<>(
|
||||
listResult.getPage(), listResult.getSize(), listResult.getTotal(), list
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<PhotoVo> listBy(String groupName) {
|
||||
var options = ListOptions.builder()
|
||||
.andQuery(QueryFactory.equal("spec.groupName", groupName))
|
||||
.build();
|
||||
return client.listAll(Photo.class, options, defaultSort()).map(PhotoVo::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<PhotoGroupVo> groupBy() {
|
||||
return this.client.listAll(PhotoGroup.class, ListOptions.builder().build(), defaultSort())
|
||||
.concatMap(group -> {
|
||||
var builder = PhotoGroupVo.from(group);
|
||||
return this.listBy(group.getMetadata().getName())
|
||||
.collectList()
|
||||
.doOnNext(photos -> {
|
||||
PhotoGroup.PostGroupStatus status = group.getStatus();
|
||||
status.setPhotoCount(photos.size());
|
||||
builder.status(status);
|
||||
builder.photos(photos);
|
||||
})
|
||||
.then(Mono.fromSupplier(builder::build));
|
||||
});
|
||||
}
|
||||
|
||||
static Sort defaultSort() {
|
||||
return Sort.by(
|
||||
asc("spec.priority"),
|
||||
desc("metadata.creationTimestamp"),
|
||||
asc("metadata.name")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package run.halo.photos.service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||
import run.halo.photos.PhotoGroup;
|
||||
|
||||
/**
|
||||
* A service for {@link PhotoGroup}.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface PhotoGroupService {
|
||||
|
||||
/**
|
||||
* List photo groups.
|
||||
*
|
||||
* @param request request
|
||||
* @return a mono of list result
|
||||
*/
|
||||
Mono<ListResult<PhotoGroup>> listPhotoGroup(QueryListRequest request);
|
||||
|
||||
/**
|
||||
* Create a photo group.
|
||||
*
|
||||
* @param name name
|
||||
* @return a mono of photo group
|
||||
*/
|
||||
Mono<PhotoGroup> deletePhotoGroup(String name);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package run.halo.photos.service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.photos.Photo;
|
||||
import run.halo.photos.PhotoQuery;
|
||||
|
||||
/**
|
||||
* A service for {@link Photo}.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface PhotoService {
|
||||
|
||||
/**
|
||||
* List photos.
|
||||
*
|
||||
* @param query query
|
||||
* @return a mono of list result
|
||||
*/
|
||||
Mono<ListResult<Photo>> listPhoto(PhotoQuery query);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package run.halo.photos.service.impl;
|
||||
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||
import run.halo.photos.Photo;
|
||||
import run.halo.photos.PhotoGroup;
|
||||
import run.halo.photos.service.PhotoGroupService;
|
||||
|
||||
/**
|
||||
* Service implementation for {@link Photo}.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Component
|
||||
class PhotoGroupServiceImpl implements PhotoGroupService {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public PhotoGroupServiceImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<PhotoGroup>> listPhotoGroup(QueryListRequest query) {
|
||||
return this.client.listBy(
|
||||
PhotoGroup.class,
|
||||
toListOptions(query),
|
||||
PageRequestImpl.of(query.getPage(), query.getSize())
|
||||
)
|
||||
.flatMap(listResult -> Flux.fromStream(listResult.get())
|
||||
.flatMap(this::populatePhotos)
|
||||
.collectList()
|
||||
.map(groups -> new ListResult<>(
|
||||
listResult.getPage(),
|
||||
listResult.getSize(),
|
||||
listResult.getTotal(),
|
||||
groups
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<PhotoGroup> deletePhotoGroup(String name) {
|
||||
return this.client.fetch(PhotoGroup.class, name)
|
||||
.flatMap(this.client::delete)
|
||||
.flatMap(deleted -> {
|
||||
var listOptions = ListOptions.builder()
|
||||
.andQuery(QueryFactory.equal("spec.groupName", name))
|
||||
.build();
|
||||
return this.client.listAll(Photo.class, listOptions, Sort.unsorted())
|
||||
.flatMap(this.client::delete)
|
||||
.then()
|
||||
.thenReturn(deleted);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<PhotoGroup> populatePhotos(PhotoGroup photoGroup) {
|
||||
return fetchPhotoCount(photoGroup)
|
||||
.doOnNext(count -> photoGroup.getStatusOrDefault().setPhotoCount(count))
|
||||
.thenReturn(photoGroup);
|
||||
}
|
||||
|
||||
Mono<Integer> fetchPhotoCount(PhotoGroup photoGroup) {
|
||||
Assert.notNull(photoGroup, "The photoGroup must not be null.");
|
||||
String name = photoGroup.getMetadata().getName();
|
||||
return client.list(
|
||||
Photo.class,
|
||||
photo -> !photo.isDeleted() && photo.getSpec().getGroupName().equals(name),
|
||||
null
|
||||
)
|
||||
.count()
|
||||
.defaultIfEmpty(0L)
|
||||
.map(Long::intValue);
|
||||
}
|
||||
|
||||
ListOptions toListOptions(QueryListRequest query) {
|
||||
return labelAndFieldSelectorToListOptions(
|
||||
query.getLabelSelector(), query.getFieldSelector()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package run.halo.photos.service.impl;
|
||||
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
import run.halo.photos.Photo;
|
||||
import run.halo.photos.PhotoQuery;
|
||||
import run.halo.photos.service.PhotoService;
|
||||
|
||||
/**
|
||||
* Service implementation for {@link Photo}.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Component
|
||||
class PhotoServiceImpl implements PhotoService {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public PhotoServiceImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<Photo>> listPhoto(PhotoQuery query) {
|
||||
return this.client.listBy(
|
||||
Photo.class,
|
||||
toListOptions(query),
|
||||
PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort())
|
||||
);
|
||||
}
|
||||
|
||||
ListOptions toListOptions(PhotoQuery query) {
|
||||
var builder = ListOptions.builder(labelAndFieldSelectorToListOptions(
|
||||
query.getLabelSelector(), query.getFieldSelector())
|
||||
);
|
||||
|
||||
if (StringUtils.isNotBlank(query.getKeyword())) {
|
||||
builder.andQuery(QueryFactory.contains("spec.displayName", query.getKeyword()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(query.getGroup())) {
|
||||
builder.andQuery(QueryFactory.equal("spec.groupName", query.getGroup()));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package run.halo.photos.vo;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.theme.finders.vo.ExtensionVoOperator;
|
||||
import run.halo.photos.PhotoGroup;
|
||||
|
||||
/**
|
||||
* @author LIlGG
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class PhotoGroupVo implements ExtensionVoOperator {
|
||||
MetadataOperator metadata;
|
||||
|
||||
PhotoGroup.PhotoGroupSpec spec;
|
||||
|
||||
PhotoGroup.PostGroupStatus status;
|
||||
|
||||
List<PhotoVo> photos;
|
||||
|
||||
public static PhotoGroupVoBuilder from(PhotoGroup photoGroup) {
|
||||
return PhotoGroupVo.builder()
|
||||
.metadata(photoGroup.getMetadata())
|
||||
.spec(photoGroup.getSpec())
|
||||
.status(photoGroup.getStatusOrDefault())
|
||||
.photos(List.of());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package run.halo.photos.vo;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.theme.finders.vo.ExtensionVoOperator;
|
||||
import run.halo.photos.Photo;
|
||||
|
||||
/**
|
||||
* @author LIlGG
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class PhotoVo implements ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
Photo.PhotoSpec spec;
|
||||
|
||||
public static PhotoVo from(Photo photo) {
|
||||
return PhotoVo.builder()
|
||||
.metadata(photo.getMetadata())
|
||||
.spec(photo.getSpec())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-photos-view
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "图库"
|
||||
rbac.authorization.halo.run/ui-permissions: '["plugin:photos:view"]'
|
||||
rbac.authorization.halo.run/display-name: "图库查看"
|
||||
rules:
|
||||
- apiGroups: [ "core.halo.run", "console.api.photo.halo.run"]
|
||||
resources: [ "photos", "photogroups" ]
|
||||
verbs: [ "get", "list" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: role-template-photos-manage
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/dependencies: '["role-template-photos-view"]'
|
||||
rbac.authorization.halo.run/ui-permissions: '["plugin:photos:manage"]'
|
||||
rbac.authorization.halo.run/module: "图库"
|
||||
rbac.authorization.halo.run/display-name: "图库管理"
|
||||
rules:
|
||||
- apiGroups: [ "core.halo.run", "console.api.photo.halo.run"]
|
||||
resources: [ "photos", "photogroups" ]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1alpha1
|
||||
kind: Setting
|
||||
metadata:
|
||||
name: plugin-photos-settings
|
||||
spec:
|
||||
forms:
|
||||
- group: base
|
||||
label: 基本设置
|
||||
formSchema:
|
||||
- $formkit: text
|
||||
label: 页面标题
|
||||
name: title
|
||||
validation: required
|
||||
value: "图库"
|
||||
- $formkit: text
|
||||
label: 相册列表显示条数
|
||||
name: pageSize
|
||||
validation: required|Number
|
||||
value: 20
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class=""><rect id="r4" width="512" height="512" x="0" y="0" rx="" fill="#8A8AFF" stroke="#FFFFFF" stroke-width="0" stroke-opacity="100%" paint-order="stroke"></rect><clipPath id="clip"><use xlink:href="#r4"></use></clipPath><defs><linearGradient id="r5" gradientUnits="userSpaceOnUse" gradientTransform="rotate(180)" style="transform-origin: center center;"><stop stop-color="#8A8AFF"></stop><stop offset="1" stop-color="#596AA1"></stop></linearGradient><radialGradient id="r6" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(256) rotate(90) scale(512)"><stop stop-color="white"></stop><stop offset="1" stop-color="white" stop-opacity="0"></stop></radialGradient></defs><svg xmlns="http://www.w3.org/2000/svg" width="352" height="352" viewBox="0 0 24 24" x="80" y="80" alignment-baseline="middle" style="color: rgb(255, 255, 255);"><path fill="currentColor" d="m5 11.1l2-2l5.5 5.5l3.5-3.5l3 3V5H5v6.1Zm0 2.829V19h3.1l2.986-2.985L7 11.929l-2 2ZM10.929 19H19v-2.071l-3-3L10.929 19ZM4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Zm11.5 7a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3Z"></path></svg></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,26 @@
|
||||
apiVersion: plugin.halo.run/v1alpha1
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: PluginPhotos
|
||||
annotations:
|
||||
# Add supports for Halo App Store
|
||||
# https://www.halo.run/store/apps/app-BmQJW
|
||||
"store.halo.run/app-id": "app-BmQJW"
|
||||
spec:
|
||||
enabled: true
|
||||
version: 1.0.0
|
||||
requires: ">=2.21.0"
|
||||
author:
|
||||
name: Halo
|
||||
website: https://github.com/halo-dev
|
||||
logo: logo.svg
|
||||
homepage: https://www.halo.run/store/apps/app-BmQJW
|
||||
repo: https://github.com/halo-sigs/plugin-photos
|
||||
issues: https://github.com/halo-sigs/plugin-photos/issues
|
||||
configMapName: plugin-photos-configmap
|
||||
settingName: plugin-photos-settings
|
||||
displayName: "图库管理"
|
||||
description: "图库管理模块"
|
||||
license:
|
||||
- name: "GPL-3.0"
|
||||
url: "https://github.com/halo-sigs/plugin-photos/blob/main/LICENSE"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 345 KiB |
@@ -0,0 +1 @@
|
||||
花径不曾缘客扫,蓬门今始为君开
|
||||
@@ -0,0 +1 @@
|
||||
<div>测试中文乱码 hello apples</div>
|
||||
Reference in New Issue
Block a user