1.5.1原版

This commit is contained in:
anian
2026-03-09 21:13:30 +08:00
commit 65174cb3df
55 changed files with 10364 additions and 0 deletions
@@ -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;
}
+49
View File
@@ -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
+1
View File
@@ -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

+26
View File
@@ -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

+1
View File
@@ -0,0 +1 @@
花径不曾缘客扫,蓬门今始为君开
+1
View File
@@ -0,0 +1 @@
<div>测试中文乱码 hello apples</div>